NDEF Push Protocol implementation.

The protocol allows pushing NDEF messages
over LLCP in order to simulate a card read
wihtout having to use card emulation. This
also allows for 2 way transfer of meesages.

The protocol allows for a single immediate
message, to be dispatched upon arrival as if
it were read from a tag, and any number of
deferred dispatch messages. The handling of
deferred dispatch messages is up to the
receiver and not implemented in this patch.

Change-Id: Ib99e4fc01532cc741debab370a417f94669b62ac
diff --git a/src/com/android/nfc/NfcService.java b/src/com/android/nfc/NfcService.java
index ff888ea..098a0aa 100755
--- a/src/com/android/nfc/NfcService.java
+++ b/src/com/android/nfc/NfcService.java
@@ -18,14 +18,14 @@
 
 import com.android.internal.nfc.LlcpServiceSocket;
 import com.android.internal.nfc.LlcpSocket;
-import com.android.nfc.mytag.MyTagClient;
-import com.android.nfc.mytag.MyTagServer;
+import com.android.nfc.ndefpush.NdefPushClient;
+import com.android.nfc.ndefpush.NdefPushServer;
 
 import android.app.Activity;
 import android.app.Application;
 import android.app.PendingIntent;
-import android.app.PendingIntent.CanceledException;
 import android.app.StatusBarManager;
+import android.app.PendingIntent.CanceledException;
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -252,8 +252,8 @@
     private SharedPreferences mPrefs;
     private SharedPreferences.Editor mPrefsEditor;
     private PowerManager.WakeLock mWakeLock;
-    private MyTagServer mMyTagServer;
-    private MyTagClient mMyTagClient;
+    NdefPushClient mNdefPushClient;
+    NdefPushServer mNdefPushServer;
 
     private static NfcService sService;
 
@@ -273,8 +273,8 @@
         mManager = new NativeNfcManager(mContext, this);
         mManager.initializeNativeStructure();
 
-        mMyTagServer = new MyTagServer();
-        mMyTagClient = new MyTagClient(this);
+        mNdefPushClient = new NdefPushClient(this);
+        mNdefPushServer = new NdefPushServer();
 
         mSecureElement = new NativeNfcSecureElement();
 
@@ -339,7 +339,7 @@
 
             if (previouslyEnabled) {
                 /* tear down the my tag server */
-                mMyTagServer.stop();
+                mNdefPushServer.stop();
                 isSuccess = mManager.deinitialize();
                 if (DBG) Log.d(TAG, "NFC success of deinitialize = " + isSuccess);
                 if (isSuccess) {
@@ -361,10 +361,10 @@
         public void enableForegroundDispatch(ComponentName activity, PendingIntent intent,
                 IntentFilter[] filters) {
             mContext.enforceCallingOrSelfPermission(NFC_PERM, NFC_PERM_ERROR);
+            if (activity == null || filters == null || filters.length == 0 || intent == null) {
+                throw new IllegalArgumentException();
+            }
             synchronized (this) {
-                if (activity == null || filters == null || filters.length == 0 || intent == null) {
-                    throw new IllegalArgumentException();
-                }
                 if (mDispatchOverrideFilters != null) {
                     Log.e(TAG, "Replacing active dispatch overrides");
                 }
@@ -386,6 +386,25 @@
         }
 
         @Override
+        public void enableForegroundNdefPush(ComponentName activity, NdefMessage msg) {
+            mContext.enforceCallingOrSelfPermission(NFC_PERM, NFC_PERM_ERROR);
+            if (activity == null || msg == null) {
+                throw new IllegalArgumentException();
+            }
+            if (mNdefPushClient.setForegroundMessage(msg)) {
+                Log.e(TAG, "Replacing active NDEF push message");
+            }
+        }
+
+        @Override
+        public void disableForegroundNdefPush(ComponentName activity) {
+            mContext.enforceCallingOrSelfPermission(NFC_PERM, NFC_PERM_ERROR);
+            if (!mNdefPushClient.setForegroundMessage(null)) {
+                Log.e(TAG, "No active foreground NDEF push message");
+            }
+        }
+
+        @Override
         public int createLlcpConnectionlessSocket(int sap) throws RemoteException {
             mContext.enforceCallingOrSelfPermission(NFC_PERM, NFC_PERM_ERROR);
 
@@ -2142,7 +2161,7 @@
             maybeEnableDiscovery();
 
             /* bring up the my tag server */
-            mMyTagServer.start();
+            mNdefPushServer.start();
 
         } else {
             mIsNfcEnabled = false;
diff --git a/src/com/android/nfc/mytag/MyTagClient.java b/src/com/android/nfc/mytag/MyTagClient.java
deleted file mode 100755
index 613fad4..0000000
--- a/src/com/android/nfc/mytag/MyTagClient.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2010 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.mytag;
-
-import com.android.internal.nfc.LlcpException;
-import com.android.internal.nfc.LlcpSocket;
-import com.android.nfc.NfcService;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.nfc.NdefMessage;
-import android.nfc.NfcAdapter;
-import android.os.AsyncTask;
-import android.util.Log;
-
-import java.io.IOException;
-
-/**
- * Simple client to push the local NDEF message to a server on the remote side of an
- * LLCP connection. The message is set via {@link NfcAdapter#setLocalNdefMessage}.
- */
-public class MyTagClient extends BroadcastReceiver {
-    private static final String TAG = "MyTagClient";
-    private static final int MIU = 128;
-    private static final boolean DBG = true;
-
-    public MyTagClient(Context context) {
-        context.registerReceiver(this, new IntentFilter(NfcAdapter.ACTION_LLCP_LINK_STATE_CHANGED));
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        int linkState = intent.getIntExtra(NfcAdapter.EXTRA_LLCP_LINK_STATE_CHANGED,
-                NfcAdapter.LLCP_LINK_STATE_DEACTIVATED);
-        if (linkState != NfcAdapter.LLCP_LINK_STATE_ACTIVATED) {
-            // The link was torn down, ignore
-            return;
-        }
-
-        if (DBG) Log.d(TAG, "LLCP connection up and running");
-        NfcAdapter adapter = NfcAdapter.getDefaultAdapter(context);
-        NdefMessage msg = adapter.getLocalNdefMessage();
-        if (msg == null) {
-            if (DBG) Log.d(TAG, "No MyTag set, exiting");
-            // Nothing to send to the server
-            return;
-        }
-
-        new SendAsync().execute(msg);
-    }
-
-    final class SendAsync extends AsyncTask<NdefMessage, Void, Void> {
-        private void trace(String msg) {
-            if (DBG) Log.d(TAG, Thread.currentThread().getId() + ": " + msg);
-        }
-        private void error(String msg, Throwable e) {
-            if (DBG) Log.e(TAG, Thread.currentThread().getId() + ": " + msg, e);
-        }
-
-        @Override
-        public Void doInBackground(NdefMessage... msgs) {
-            NfcService service = NfcService.getInstance();
-            NdefMessage msg = msgs[0];
-            byte[] buffer = msg.toByteArray();
-            int offset = 0;
-            int remoteMiu;
-            LlcpSocket sock = null;
-            try {
-                trace("about to create socket");
-                // Connect to the my tag server on the remote side
-                sock = service.createLlcpSocket(0, MIU, 1, 1024);
-                trace("about to connect");
-//                sock.connect(MyTagServer.SERVICE_NAME);
-                sock.connect(0x20);
-
-                remoteMiu = sock.getRemoteSocketMiu();
-                trace("about to send a " + buffer.length + "-bytes message");
-                while (offset < buffer.length) {
-                    int length = buffer.length - offset;
-                    if (length > remoteMiu) {
-                        length = remoteMiu;
-                    }
-                    byte[] tmpBuffer = new byte[length];
-                    System.arraycopy(buffer, offset, tmpBuffer, 0, length);
-                    trace("about to send a " + length + "-bytes packet");
-                    sock.send(tmpBuffer);
-                    offset += length;
-                }
-            } catch (IOException e) {
-                error("couldn't send tag", e);
-            } catch (LlcpException e) {
-                // Most likely the other side doesn't support the my tag protocol
-                error("couldn't send tag", e);
-            } finally {
-                if (sock != null) {
-                    try {
-                        trace("about to close");
-                        sock.close();
-                    } catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-            return null;
-        }
-    }
-}
diff --git a/src/com/android/nfc/ndefpush/NdefPushClient.java b/src/com/android/nfc/ndefpush/NdefPushClient.java
new file mode 100755
index 0000000..8d7c9a8
--- /dev/null
+++ b/src/com/android/nfc/ndefpush/NdefPushClient.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2010 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.ndefpush;
+
+import com.android.internal.nfc.LlcpException;
+import com.android.internal.nfc.LlcpSocket;
+import com.android.nfc.NfcService;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nfc.NdefMessage;
+import android.nfc.NfcAdapter;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Simple client to push the local NDEF message to a server on the remote side of an
+ * LLCP connection. The message is set via {@link NfcAdapter#setLocalNdefMessage}.
+ */
+public class NdefPushClient extends BroadcastReceiver {
+    private static final String TAG = "NdefPushClient";
+    private static final int MIU = 128;
+    private static final boolean DBG = true;
+
+    /** Locked on MyTagClient.class */
+    NdefMessage mForegroundMsg;
+
+    public NdefPushClient(Context context) {
+        context.registerReceiver(this, new IntentFilter(NfcAdapter.ACTION_LLCP_LINK_STATE_CHANGED));
+    }
+
+    public boolean setForegroundMessage(NdefMessage msg) {
+        synchronized (this) {
+            boolean set = mForegroundMsg != null;
+            mForegroundMsg = msg;
+            return set;
+        }
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        int linkState = intent.getIntExtra(NfcAdapter.EXTRA_LLCP_LINK_STATE_CHANGED,
+                NfcAdapter.LLCP_LINK_STATE_DEACTIVATED);
+        if (linkState != NfcAdapter.LLCP_LINK_STATE_ACTIVATED) {
+            // The link was torn down, ignore
+            return;
+        }
+
+        if (DBG) Log.d(TAG, "LLCP connection up and running");
+
+        NdefMessage foregroundMsg;
+        synchronized (this) {
+            foregroundMsg = mForegroundMsg;
+        }
+
+        NfcAdapter adapter = NfcAdapter.getDefaultAdapter(context);
+        NdefMessage myTag = adapter.getLocalNdefMessage();
+
+        if (foregroundMsg != null && myTag != null) {
+            if (DBG) Log.d(TAG, "sending foreground and my tag");
+            new SendAsync().execute(foregroundMsg, myTag);
+        } else if (myTag != null) {
+            if (DBG) Log.d(TAG, "sending my tag");
+            new SendAsync().execute(myTag);
+        } else if (foregroundMsg != null) {
+            if (DBG) Log.d(TAG, "sending foreground");
+            new SendAsync().execute(foregroundMsg);
+        } else {
+            if (DBG) Log.d(TAG, "no tags set, bailing");
+            return;
+        }
+    }
+
+    final class SendAsync extends AsyncTask<NdefMessage, Void, Void> {
+        @Override
+        public Void doInBackground(NdefMessage... msgs) {
+            NfcService service = NfcService.getInstance();
+
+            // We only handle a single immediate action for now
+            NdefPushProtocol msg = new NdefPushProtocol(msgs[0], NdefPushProtocol.ACTION_IMMEDIATE);
+            byte[] buffer = msg.toByteArray();
+            int offset = 0;
+            int remoteMiu;
+            LlcpSocket sock = null;
+            try {
+                if (DBG) Log.d(TAG, "about to create socket");
+                // Connect to the my tag server on the remote side
+                sock = service.createLlcpSocket(0, MIU, 1, 1024);
+                if (DBG) Log.d(TAG, "about to connect to service " + NdefPushServer.SERVICE_NAME);
+                sock.connect(NdefPushServer.SERVICE_NAME);
+
+                remoteMiu = sock.getRemoteSocketMiu();
+                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, length);
+                    if (DBG) Log.d(TAG, "about to send a " + length + " byte packet");
+                    sock.send(tmpBuffer);
+                    offset += length;
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "couldn't send tag");
+                if (DBG) Log.d(TAG, "exception:", e);
+            } catch (LlcpException e) {
+                // Most likely the other side doesn't support the my tag protocol
+                Log.e(TAG, "couldn't send tag");
+                if (DBG) Log.d(TAG, "exception:", e);
+            } finally {
+                if (sock != null) {
+                    try {
+                        if (DBG) Log.d(TAG, "about to close");
+                        sock.close();
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+            }
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/nfc/ndefpush/NdefPushProtocol.java b/src/com/android/nfc/ndefpush/NdefPushProtocol.java
new file mode 100644
index 0000000..5520615
--- /dev/null
+++ b/src/com/android/nfc/ndefpush/NdefPushProtocol.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2010 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.ndefpush;
+
+import android.util.Log;
+
+import android.nfc.NdefMessage;
+import android.nfc.FormatException;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+
+/**
+ * Implementation of the NDEF Push Protocol.
+ */
+public class NdefPushProtocol {
+    public static final byte ACTION_IMMEDIATE = (byte) 0x01;
+    public static final byte ACTION_BACKGROUND = (byte) 0x02;
+
+    private static final String TAG = "NdefMessageSet";
+    private static final byte VERSION = 1;
+
+    private int mNumMessages;
+    private byte[] mActions;
+    private NdefMessage[] mMessages;
+
+    public NdefPushProtocol(NdefMessage msg, byte action) {
+        mNumMessages = 1;
+        mActions = new byte[1];
+        mActions[0] = action;
+        mMessages = new NdefMessage[1];
+        mMessages[0] = msg;
+    }
+
+    public NdefPushProtocol(byte[] actions, NdefMessage[] messages) {
+        if (actions.length != messages.length || actions.length == 0) {
+            throw new IllegalArgumentException(
+                    "actions and messages must be the same size and non-empty");
+        }
+
+        // Keep a copy of these arrays
+        int numMessages = actions.length;
+        mActions = new byte[numMessages];
+        System.arraycopy(actions, 0, mActions, 0, numMessages);
+        mMessages = new NdefMessage[numMessages];
+        System.arraycopy(messages, 0, mMessages, 0, numMessages);
+        mNumMessages = numMessages;
+    }
+
+    public NdefPushProtocol(byte[] data) throws FormatException {
+        ByteArrayInputStream buffer = new ByteArrayInputStream(data);
+        DataInputStream input = new DataInputStream(buffer);
+
+        // Check version of protocol
+        byte version;
+        try {
+            version = input.readByte();
+        } catch (java.io.IOException e) {
+            Log.w(TAG, "Unable to read version");
+            throw new FormatException("Unable to read version");
+        }
+
+        if(version != VERSION) {
+            Log.w(TAG, "Got version " + version + ",  expected " + VERSION);
+            throw new FormatException("Got version " + version + ",  expected " + VERSION);
+        }
+
+        // Read numMessages
+        try {
+            mNumMessages = input.readInt();
+        } catch(java.io.IOException e) {
+            Log.w(TAG, "Unable to read numMessages");
+            throw new FormatException("Error while parsing NdefMessageSet");
+        }
+        if(mNumMessages == 0) {
+            Log.w(TAG, "No NdefMessage inside NdefMessageSet packet");
+            throw new FormatException("Error while parsing NdefMessageSet");
+        }
+
+        // Read actions and messages
+        mActions = new byte[mNumMessages];
+        mMessages = new NdefMessage[mNumMessages];
+        for(int i = 0; i < mNumMessages; i++) {
+            // Read action
+            try {
+                mActions[i] = input.readByte();
+            } catch(java.io.IOException e) {
+                Log.w(TAG, "Unable to read action for message " + i);
+                throw new FormatException("Error while parsing NdefMessageSet");
+            }
+            // Read message length
+            int length;
+            try {
+                length = input.readInt();
+            } catch(java.io.IOException e) {
+                Log.w(TAG, "Unable to read length for message " + i);
+                throw new FormatException("Error while parsing NdefMessageSet");
+            }
+            byte[] bytes = new byte[length];
+            // Read message
+            int lengthRead;
+            try {
+                lengthRead = input.read(bytes);
+            } catch(java.io.IOException e) {
+                Log.w(TAG, "Unable to read bytes for message " + i);
+                throw new FormatException("Error while parsing NdefMessageSet");
+            }
+            if(length != lengthRead) {
+                Log.w(TAG, "Read " + lengthRead + " bytes but expected " +
+                    length);
+                throw new FormatException("Error while parsing NdefMessageSet");
+            }
+            // Create and store NdefMessage
+            try {
+                mMessages[i] = new NdefMessage(bytes);
+            } catch(FormatException e) {
+                throw e;
+            }
+        }
+    }
+
+    public NdefMessage getImmediate() {
+        // Find and return the first immediate message
+        for(int i = 0; i < mNumMessages; i++) {
+            if(mActions[i] == ACTION_IMMEDIATE) {
+                return mMessages[i];
+            }
+        }
+        return null;
+    }
+
+    public byte[] toByteArray() {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024);
+        DataOutputStream output = new DataOutputStream(buffer);
+
+        try {
+            output.writeByte(VERSION);
+            output.writeInt(mNumMessages);
+            for(int i = 0; i < mNumMessages; i++) {
+                output.writeByte(mActions[i]);
+                byte[] bytes = mMessages[i].toByteArray();
+                output.writeInt(bytes.length);
+                output.write(bytes);
+            }
+        } catch(java.io.IOException e) {
+            return null;
+        }
+
+        return buffer.toByteArray();
+    }
+}
diff --git a/src/com/android/nfc/mytag/MyTagServer.java b/src/com/android/nfc/ndefpush/NdefPushServer.java
similarity index 91%
rename from src/com/android/nfc/mytag/MyTagServer.java
rename to src/com/android/nfc/ndefpush/NdefPushServer.java
index 0b7c426..4073567 100755
--- a/src/com/android/nfc/mytag/MyTagServer.java
+++ b/src/com/android/nfc/ndefpush/NdefPushServer.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.nfc.mytag;
+package com.android.nfc.ndefpush;
 
 import com.android.internal.nfc.LlcpException;
 import com.android.internal.nfc.LlcpServiceSocket;
@@ -22,7 +22,6 @@
 import com.android.nfc.NfcService;
 
 import android.nfc.FormatException;
-import android.nfc.NdefMessage;
 import android.nfc.NfcAdapter;
 import android.util.Log;
 
@@ -33,14 +32,14 @@
  * A simple server that accepts NDEF messages pushed to it over an LLCP connection. Those messages
  * are typically set on the client side by using {@link NfcAdapter#setLocalNdefMessage}.
  */
-public class MyTagServer {
-    private static final String TAG = "MyTagServer";
+public class NdefPushServer {
+    private static final String TAG = "NdefPushServer";
     private static final boolean DBG = true;
 
-    private static final int SERVICE_SAP = 0x20;
+    private static final int SERVICE_SAP = 0x10;
     private static final int MIU = 256;
 
-    static final String SERVICE_NAME = "com.android.mytag";
+    static final String SERVICE_NAME = "com.android.npp";
 
     NfcService mService = NfcService.getInstance();
 
@@ -52,7 +51,7 @@
         private LlcpSocket mSock;
 
         ConnectionThread(LlcpSocket sock) {
-            super("MyTagServer");
+            super(TAG);
             mSock = sock;
         }
 
@@ -83,12 +82,12 @@
                     }
                 }
 
-                // Build NDEF message from the stream
-                NdefMessage msg = new NdefMessage(buffer.toByteArray());
+                // Build NDEF message set from the stream
+                NdefPushProtocol msg = new NdefPushProtocol(buffer.toByteArray());
                 if (DBG) Log.d(TAG, "got message " + msg.toString());
 
                 // Send the intent for the fake tag
-                mService.sendMockNdefTag(msg);
+                mService.sendMockNdefTag(msg.getImmediate());
             } catch (FormatException e) {
                 Log.e(TAG, "badly formatted NDEF message, ignoring", e);
             } finally {
@@ -112,7 +111,7 @@
         public void run() {
             while (mRunning) {
                 if (DBG) Log.d(TAG, "about create LLCP service socket");
-                mServerSocket = mService.createLlcpServiceSocket(SERVICE_SAP, null,
+                mServerSocket = mService.createLlcpServiceSocket(SERVICE_SAP, SERVICE_NAME,
                         MIU, 1, 1024);
                 if (mServerSocket == null) {
                     if (DBG) Log.d(TAG, "failed to create LLCP service socket");