diff options
Diffstat (limited to 'src/com')
-rwxr-xr-x | src/com/android/nfc/P2pLinkManager.java | 38 | ||||
-rw-r--r-- | src/com/android/nfc/echoserver/EchoServer.java | 393 |
2 files changed, 431 insertions, 0 deletions
diff --git a/src/com/android/nfc/P2pLinkManager.java b/src/com/android/nfc/P2pLinkManager.java index 466453d..277e739 100755 --- a/src/com/android/nfc/P2pLinkManager.java +++ b/src/com/android/nfc/P2pLinkManager.java @@ -16,6 +16,7 @@ package com.android.nfc; +import com.android.nfc.echoserver.EchoServer; import com.android.nfc.ndefpush.NdefPushClient; import com.android.nfc.ndefpush.NdefPushServer; import com.android.nfc.snep.SnepClient; @@ -97,6 +98,11 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba static final String TAG = "NfcP2pLinkManager"; static final boolean DBG = true; + /** Enables the LLCP EchoServer, which can be used to test the android + * LLCP stack against nfcpy. + */ + static final boolean ECHOSERVER_ENABLED = false; + // TODO dynamically assign SAP values static final int NDEFPUSH_SAP = 0x10; @@ -105,6 +111,8 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba static final int MSG_DEBOUNCE_TIMEOUT = 1; static final int MSG_RECEIVE_COMPLETE = 2; static final int MSG_SEND_COMPLETE = 3; + static final int MSG_START_ECHOSERVER = 4; + static final int MSG_STOP_ECHOSERVER = 5; // values for mLinkState static final int LINK_STATE_DOWN = 1; @@ -123,6 +131,7 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba final NdefPushServer mNdefPushServer; final SnepServer mDefaultSnepServer; + final EchoServer mEchoServer; final ActivityManager mActivityManager; final PackageManager mPackageManager; final Context mContext; @@ -144,6 +153,11 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba public P2pLinkManager(Context context) { mNdefPushServer = new NdefPushServer(NDEFPUSH_SAP, mNppCallback); mDefaultSnepServer = new SnepServer(mDefaultSnepCallback); + if (ECHOSERVER_ENABLED) { + mEchoServer = new EchoServer(); + } else { + mEchoServer = null; + } mActivityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); mPackageManager = context.getPackageManager(); mContext = context; @@ -166,9 +180,15 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba if (!mIsReceiveEnabled && receiveEnable) { mDefaultSnepServer.start(); mNdefPushServer.start(); + if (mEchoServer != null) { + mHandler.sendEmptyMessage(MSG_START_ECHOSERVER); + } } else if (mIsReceiveEnabled && !receiveEnable) { mDefaultSnepServer.stop(); mNdefPushServer.stop(); + if (mEchoServer != null) { + mHandler.sendEmptyMessage(MSG_STOP_ECHOSERVER); + } } mIsSendEnabled = sendEnable; mIsReceiveEnabled = receiveEnable; @@ -196,6 +216,10 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba Log.i(TAG, "LLCP activated"); synchronized (P2pLinkManager.this) { + if (mEchoServer != null) { + mEchoServer.onLlcpActivated(); + } + switch (mLinkState) { case LINK_STATE_DOWN: mLinkState = LINK_STATE_UP; @@ -278,6 +302,10 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba public void onLlcpDeactivated() { Log.i(TAG, "LLCP deactivated."); synchronized (this) { + if (mEchoServer != null) { + mEchoServer.onLlcpDeactivated(); + } + switch (mLinkState) { case LINK_STATE_DOWN: case LINK_STATE_DEBOUNCE: @@ -410,6 +438,16 @@ public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callba @Override public boolean handleMessage(Message msg) { switch (msg.what) { + case MSG_START_ECHOSERVER: + synchronized (this) { + mEchoServer.start(); + break; + } + case MSG_STOP_ECHOSERVER: + synchronized (this) { + mEchoServer.stop(); + break; + } case MSG_DEBOUNCE_TIMEOUT: synchronized (this) { if (mLinkState != LINK_STATE_DEBOUNCE) { diff --git a/src/com/android/nfc/echoserver/EchoServer.java b/src/com/android/nfc/echoserver/EchoServer.java new file mode 100644 index 0000000..bd060f7 --- /dev/null +++ b/src/com/android/nfc/echoserver/EchoServer.java @@ -0,0 +1,393 @@ +/* + * 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.echoserver; + +import com.android.nfc.DeviceHost.LlcpConnectionlessSocket; +import com.android.nfc.LlcpException; +import com.android.nfc.DeviceHost.LlcpServerSocket; +import com.android.nfc.DeviceHost.LlcpSocket; +import com.android.nfc.LlcpPacket; +import com.android.nfc.NfcService; + +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * EchoServer is an implementation of the echo server that is used in the + * nfcpy LLCP test suite. Enabling the EchoServer allows to test Android + * NFC devices against nfcpy. + * It has two main features (which run simultaneously): + * 1) A connection-based server, which has a receive buffer of two + * packets. Once a packet is received, a 2-second sleep is initiated. + * After these 2 seconds, all packets that are in the receive buffer + * are echoed back on the same connection. The connection-based server + * does not drop packets, but simply blocks if the queue is full. + * 2) A connection-less mode, which has a receive buffer of two packets. + * On LLCP link activation, we try to receive data on a pre-determined + * connection-less SAP. Like the connection-based server, all data in + * the buffer is echoed back to the SAP from which the data originated + * after a sleep of two seconds. + * The main difference is that the connection-less SAP is supposed + * to drop packets when the buffer is full. + * + * To use with nfcpy: + * - Adapt default_miu (see ECHO_MIU below) + * - llcp-test-client.py --mode=target --co-echo=17 --cl-echo=18 -t 1 + * + * Modify -t to execute the different tests. + * + */ +public class EchoServer { + static boolean DBG = true; + + static final int DEFAULT_CO_SAP = 0x11; + static final int DEFAULT_CL_SAP = 0x12; + + // Link MIU + static final int MIU = 128; + + static final String TAG = "EchoServer"; + static final String CONNECTION_SERVICE_NAME = "urn:nfc:sn:co-echo"; + static final String CONNECTIONLESS_SERVICE_NAME = "urn:nfc:sn:cl-echo"; + + ServerThread mServerThread; + ConnectionlessServerThread mConnectionlessServerThread; + NfcService mService; + + public interface WriteCallback { + public void write(byte[] data); + } + + public EchoServer() { + mService = NfcService.getInstance(); + } + + static class EchoMachine implements Handler.Callback { + static final int QUEUE_SIZE = 2; + static final int ECHO_DELAY_IN_MS = 2000; + + /** + * ECHO_MIU must be set equal to default_miu in nfcpy. + * The nfcpy echo server is expected to maintain the + * packet boundaries and sizes of the requests - that is, + * if the nfcpy client sends a service data unit of 48 bytes + * in a packet, the echo packet should have a payload of + * 48 bytes as well. The "problem" is that the current + * Android LLCP implementation simply pushes all received data + * in a single large buffer, causing us to loose the packet + * boundaries, not knowing how much data to put in a single + * response packet. The ECHO_MIU parameter determines exactly that. + * We use ECHO_MIU=48 because of a bug in PN544, which does not respect + * the target length reduction parameter of the p2p protocol. + */ + static final int ECHO_MIU = 48; + + final BlockingQueue<byte[]> dataQueue; + final Handler handler; + final boolean dumpWhenFull; + final WriteCallback callback; + + // shutdown can be modified from multiple threads, protected by this + boolean shutdown = false; + + EchoMachine(WriteCallback callback, boolean dumpWhenFull) { + this.callback = callback; + this.dumpWhenFull = dumpWhenFull; + dataQueue = new LinkedBlockingQueue<byte[]>(QUEUE_SIZE); + handler = new Handler(this); + } + + public void pushUnit(byte[] unit, int size) { + if (dumpWhenFull && dataQueue.remainingCapacity() == 0) { + if (DBG) Log.d(TAG, "Dumping data unit"); + } else { + try { + // Split up the packet in ECHO_MIU size packets + int sizeLeft = size; + + if (dataQueue.isEmpty()) { + // First message: start echo'ing in 2 seconds + handler.sendMessageDelayed(handler.obtainMessage(), ECHO_DELAY_IN_MS); + } + + if (sizeLeft == 0) { + // Special case: also send a zero-sized data unit + dataQueue.put(new byte[] {}); + } + while (sizeLeft > 0) { + int minSize = Math.min(size, ECHO_MIU); + byte[] data = new byte[minSize]; + System.arraycopy(unit, 0, data, 0, minSize); + dataQueue.put(data); + sizeLeft -= minSize; + } + } catch (InterruptedException e) { + // Ignore + } + } + } + + /** Shuts down the EchoMachine. May block until callbacks + * in progress are completed. + */ + public synchronized void shutdown() { + dataQueue.clear(); + shutdown = true; + } + + @Override + public synchronized boolean handleMessage(Message msg) { + if (shutdown) return true; + while (!dataQueue.isEmpty()) { + callback.write(dataQueue.remove()); + } + return true; + } + } + + public class ServerThread extends Thread implements WriteCallback { + final EchoMachine echoMachine; + + boolean running = true; + LlcpServerSocket serverSocket; + LlcpSocket clientSocket; + + public ServerThread() { + super(); + echoMachine = new EchoMachine(this, false); + } + + private void handleClient(LlcpSocket socket) { + boolean connectionBroken = false; + byte[] dataUnit = new byte[1024]; + + // Get raw data from remote server + while (!connectionBroken) { + try { + int size = socket.receive(dataUnit); + if (DBG) Log.d(TAG, "read " + size + " bytes"); + if (size < 0) { + connectionBroken = true; + break; + } else { + echoMachine.pushUnit(dataUnit, size); + } + } catch (IOException e) { + // Connection broken + connectionBroken = true; + if (DBG) Log.d(TAG, "connection broken by IOException", e); + } + } + } + + @Override + public void run() { + if (DBG) Log.d(TAG, "about create LLCP service socket"); + try { + serverSocket = mService.createLlcpServerSocket(DEFAULT_CO_SAP, + CONNECTION_SERVICE_NAME, MIU, 1, 1024); + } catch (LlcpException e) { + return; + } + if (serverSocket == null) { + if (DBG) Log.d(TAG, "failed to create LLCP service socket"); + return; + } + if (DBG) Log.d(TAG, "created LLCP service socket"); + + while (running) { + + try { + if (DBG) Log.d(TAG, "about to accept"); + clientSocket = serverSocket.accept(); + if (DBG) Log.d(TAG, "accept returned " + clientSocket); + handleClient(clientSocket); + } catch (LlcpException e) { + Log.e(TAG, "llcp error", e); + running = false; + } catch (IOException e) { + Log.e(TAG, "IO error", e); + running = false; + } + } + + echoMachine.shutdown(); + + try { + clientSocket.close(); + } catch (IOException e) { + // Ignore + } + clientSocket = null; + + try { + serverSocket.close(); + } catch (IOException e) { + // Ignore + } + serverSocket = null; + } + + @Override + public void write(byte[] data) { + if (clientSocket != null) { + try { + clientSocket.send(data); + } catch (IOException e) { + // Ignore + } + } + } + + public void shutdown() { + running = false; + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + // ignore + } + serverSocket = null; + } + } + } + + public class ConnectionlessServerThread extends Thread implements WriteCallback { + final EchoMachine echoMachine; + + LlcpConnectionlessSocket socket; + int mRemoteSap; + boolean mRunning = true; + + public ConnectionlessServerThread() { + super(); + echoMachine = new EchoMachine(this, true); + } + + @Override + public void run() { + boolean connectionBroken = false; + LlcpPacket packet; + if (DBG) Log.d(TAG, "about create LLCP connectionless socket"); + try { + socket = mService.createLlcpConnectionLessSocket( + DEFAULT_CL_SAP); + if (socket == null) { + if (DBG) Log.d(TAG, "failed to create LLCP connectionless socket"); + return; + } + + while (mRunning && !connectionBroken) { + try { + packet = socket.receive(); + if (packet == null || packet.getDataBuffer() == null) { + break; + } + byte[] dataUnit = packet.getDataBuffer(); + int size = dataUnit.length; + + if (DBG) Log.d(TAG, "read " + packet.getDataBuffer().length + " bytes"); + if (size < 0) { + connectionBroken = true; + break; + } else { + mRemoteSap = packet.getRemoteSap(); + echoMachine.pushUnit(dataUnit, size); + } + } catch (IOException e) { + // Connection broken + connectionBroken = true; + if (DBG) Log.d(TAG, "connection broken by IOException", e); + } + } + } catch (LlcpException e) { + Log.e(TAG, "llcp error", e); + } finally { + echoMachine.shutdown(); + + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + } + } + } + + } + + public void shutdown() { + mRunning = false; + } + + @Override + public void write(byte[] data) { + try { + socket.send(mRemoteSap, data); + } catch (IOException e) { + if (DBG) Log.d(TAG, "Error writing data."); + } + } + } + + public void onLlcpActivated() { + synchronized (this) { + // Connectionless server can only be started once the link is up + // - otherwise, all calls to receive() on the connectionless socket + // will fail immediately. + if (mConnectionlessServerThread == null) { + mConnectionlessServerThread = new ConnectionlessServerThread(); + mConnectionlessServerThread.start(); + } + } + } + + public void onLlcpDeactivated() { + synchronized (this) { + if (mConnectionlessServerThread != null) { + mConnectionlessServerThread.shutdown(); + mConnectionlessServerThread = null; + } + } + } + + /** + * Needs to be called on the UI thread + */ + public void start() { + synchronized (this) { + if (mServerThread == null) { + mServerThread = new ServerThread(); + mServerThread.start(); + } + } + + } + + public void stop() { + synchronized (this) { + if (mServerThread != null) { + mServerThread.shutdown(); + mServerThread = null; + } + } + } +} |