blob: 71d0bf8e7b82113dab72b225f6bd2aea0913c8f5 [file] [log] [blame]
/*
* 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<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;
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;
}
}
}
}