/* * 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 = 128; final BlockingQueue 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(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; int offset = 0; 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, offset, data, 0, minSize); dataQueue.put(data); sizeLeft -= minSize; offset += 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); Log.e(TAG, "Send success!"); } catch (IOException e) { Log.e(TAG, "Send failed."); } } } 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, CONNECTIONLESS_SERVICE_NAME); 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; } } } }