1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.nfc.echoserver;
18
19import com.android.nfc.DeviceHost.LlcpConnectionlessSocket;
20import com.android.nfc.LlcpException;
21import com.android.nfc.DeviceHost.LlcpServerSocket;
22import com.android.nfc.DeviceHost.LlcpSocket;
23import com.android.nfc.LlcpPacket;
24import com.android.nfc.NfcService;
25
26import android.os.Handler;
27import android.os.Message;
28import android.util.Log;
29
30import java.io.IOException;
31import java.util.concurrent.BlockingQueue;
32import java.util.concurrent.LinkedBlockingQueue;
33
34/**
35 * EchoServer is an implementation of the echo server that is used in the
36 * nfcpy LLCP test suite. Enabling the EchoServer allows to test Android
37 * NFC devices against nfcpy.
38 * It has two main features (which run simultaneously):
39 * 1) A connection-based server, which has a receive buffer of two
40 *    packets. Once a packet is received, a 2-second sleep is initiated.
41 *    After these 2 seconds, all packets that are in the receive buffer
42 *    are echoed back on the same connection. The connection-based server
43 *    does not drop packets, but simply blocks if the queue is full.
44 * 2) A connection-less mode, which has a receive buffer of two packets.
45 *    On LLCP link activation, we try to receive data on a pre-determined
46 *    connection-less SAP. Like the connection-based server, all data in
47 *    the buffer is echoed back to the SAP from which the data originated
48 *    after a sleep of two seconds.
49 *    The main difference is that the connection-less SAP is supposed
50 *    to drop packets when the buffer is full.
51 *
52 *    To use with nfcpy:
53 *    - Adapt default_miu (see ECHO_MIU below)
54 *    - llcp-test-client.py --mode=target --co-echo=17 --cl-echo=18 -t 1
55 *
56 *    Modify -t to execute the different tests.
57 *
58 */
59public class EchoServer {
60    static boolean DBG = true;
61
62    static final int DEFAULT_CO_SAP = 0x11;
63    static final int DEFAULT_CL_SAP = 0x12;
64
65    // Link MIU
66    static final int MIU = 128;
67
68    static final String TAG = "EchoServer";
69    static final String CONNECTION_SERVICE_NAME = "urn:nfc:sn:co-echo";
70    static final String CONNECTIONLESS_SERVICE_NAME = "urn:nfc:sn:cl-echo";
71
72    ServerThread mServerThread;
73    ConnectionlessServerThread mConnectionlessServerThread;
74    NfcService mService;
75
76    public interface WriteCallback {
77        public void write(byte[] data);
78    }
79
80    public EchoServer() {
81        mService = NfcService.getInstance();
82    }
83
84    static class EchoMachine implements Handler.Callback {
85        static final int QUEUE_SIZE = 2;
86        static final int ECHO_DELAY_IN_MS = 2000;
87
88        /**
89         * ECHO_MIU must be set equal to default_miu in nfcpy.
90         * The nfcpy echo server is expected to maintain the
91         * packet boundaries and sizes of the requests - that is,
92         * if the nfcpy client sends a service data unit of 48 bytes
93         * in a packet, the echo packet should have a payload of
94         * 48 bytes as well. The "problem" is that the current
95         * Android LLCP implementation simply pushes all received data
96         * in a single large buffer, causing us to loose the packet
97         * boundaries, not knowing how much data to put in a single
98         * response packet. The ECHO_MIU parameter determines exactly that.
99         * We use ECHO_MIU=48 because of a bug in PN544, which does not respect
100         * the target length reduction parameter of the p2p protocol.
101         */
102        static final int ECHO_MIU = 128;
103
104        final BlockingQueue<byte[]> dataQueue;
105        final Handler handler;
106        final boolean dumpWhenFull;
107        final WriteCallback callback;
108
109        // shutdown can be modified from multiple threads, protected by this
110        boolean shutdown = false;
111
112        EchoMachine(WriteCallback callback, boolean dumpWhenFull) {
113            this.callback = callback;
114            this.dumpWhenFull = dumpWhenFull;
115            dataQueue = new LinkedBlockingQueue<byte[]>(QUEUE_SIZE);
116            handler = new Handler(this);
117        }
118
119        public void pushUnit(byte[] unit, int size) {
120            if (dumpWhenFull && dataQueue.remainingCapacity() == 0) {
121                if (DBG) Log.d(TAG, "Dumping data unit");
122            } else {
123                try {
124                    // Split up the packet in ECHO_MIU size packets
125                    int sizeLeft = size;
126                    int offset = 0;
127                    if (dataQueue.isEmpty()) {
128                        // First message: start echo'ing in 2 seconds
129                        handler.sendMessageDelayed(handler.obtainMessage(), ECHO_DELAY_IN_MS);
130                    }
131
132                    if (sizeLeft == 0) {
133                        // Special case: also send a zero-sized data unit
134                        dataQueue.put(new byte[] {});
135                    }
136                    while (sizeLeft > 0) {
137                        int minSize = Math.min(size, ECHO_MIU);
138                        byte[] data = new byte[minSize];
139                        System.arraycopy(unit, offset, data, 0, minSize);
140                        dataQueue.put(data);
141                        sizeLeft -= minSize;
142                        offset += minSize;
143                    }
144                } catch (InterruptedException e) {
145                    // Ignore
146                }
147            }
148        }
149
150        /** Shuts down the EchoMachine. May block until callbacks
151         *  in progress are completed.
152         */
153        public synchronized void shutdown() {
154            dataQueue.clear();
155            shutdown = true;
156        }
157
158        @Override
159        public synchronized boolean handleMessage(Message msg) {
160            if (shutdown) return true;
161            while (!dataQueue.isEmpty()) {
162                callback.write(dataQueue.remove());
163            }
164            return true;
165        }
166    }
167
168    public class ServerThread extends Thread implements WriteCallback {
169        final EchoMachine echoMachine;
170
171        boolean running = true;
172        LlcpServerSocket serverSocket;
173        LlcpSocket clientSocket;
174
175        public ServerThread() {
176            super();
177            echoMachine = new EchoMachine(this, false);
178        }
179
180        private void handleClient(LlcpSocket socket) {
181            boolean connectionBroken = false;
182            byte[] dataUnit = new byte[1024];
183
184            // Get raw data from remote server
185            while (!connectionBroken) {
186                try {
187                    int size = socket.receive(dataUnit);
188                    if (DBG) Log.d(TAG, "read " + size + " bytes");
189                    if (size < 0) {
190                        connectionBroken = true;
191                        break;
192                    } else {
193                        echoMachine.pushUnit(dataUnit, size);
194                    }
195                } catch (IOException e) {
196                    // Connection broken
197                    connectionBroken = true;
198                    if (DBG) Log.d(TAG, "connection broken by IOException", e);
199                }
200            }
201        }
202
203        @Override
204        public void run() {
205            if (DBG) Log.d(TAG, "about create LLCP service socket");
206            try {
207                serverSocket = mService.createLlcpServerSocket(DEFAULT_CO_SAP,
208                        CONNECTION_SERVICE_NAME, MIU, 1, 1024);
209            } catch (LlcpException e) {
210                return;
211            }
212            if (serverSocket == null) {
213                if (DBG) Log.d(TAG, "failed to create LLCP service socket");
214                return;
215            }
216            if (DBG) Log.d(TAG, "created LLCP service socket");
217
218            while (running) {
219
220                try {
221                    if (DBG) Log.d(TAG, "about to accept");
222                    clientSocket = serverSocket.accept();
223                    if (DBG) Log.d(TAG, "accept returned " + clientSocket);
224                    handleClient(clientSocket);
225                } catch (LlcpException e) {
226                    Log.e(TAG, "llcp error", e);
227                    running = false;
228                } catch (IOException e) {
229                    Log.e(TAG, "IO error", e);
230                    running = false;
231                }
232            }
233
234            echoMachine.shutdown();
235
236            try {
237                clientSocket.close();
238            } catch (IOException e) {
239                // Ignore
240            }
241            clientSocket = null;
242
243            try {
244                serverSocket.close();
245            } catch (IOException e) {
246                // Ignore
247            }
248            serverSocket = null;
249        }
250
251        @Override
252        public void write(byte[] data) {
253            if (clientSocket != null) {
254                try {
255                    clientSocket.send(data);
256                    Log.e(TAG, "Send success!");
257                } catch (IOException e) {
258                    Log.e(TAG, "Send failed.");
259                }
260            }
261        }
262
263        public void shutdown() {
264            running = false;
265            if (serverSocket != null) {
266                try {
267                    serverSocket.close();
268                } catch (IOException e) {
269                    // ignore
270                }
271                serverSocket = null;
272            }
273        }
274    }
275
276    public class ConnectionlessServerThread extends Thread implements WriteCallback {
277        final EchoMachine echoMachine;
278
279        LlcpConnectionlessSocket socket;
280        int mRemoteSap;
281        boolean mRunning = true;
282
283        public ConnectionlessServerThread() {
284            super();
285            echoMachine = new EchoMachine(this, true);
286        }
287
288        @Override
289        public void run() {
290            boolean connectionBroken = false;
291            LlcpPacket packet;
292            if (DBG) Log.d(TAG, "about create LLCP connectionless socket");
293            try {
294                socket = mService.createLlcpConnectionLessSocket(
295                        DEFAULT_CL_SAP, CONNECTIONLESS_SERVICE_NAME);
296                if (socket == null) {
297                    if (DBG) Log.d(TAG, "failed to create LLCP connectionless socket");
298                    return;
299                }
300
301                while (mRunning && !connectionBroken) {
302                    try {
303                        packet = socket.receive();
304                        if (packet == null || packet.getDataBuffer() == null) {
305                            break;
306                        }
307                        byte[] dataUnit = packet.getDataBuffer();
308                        int size = dataUnit.length;
309
310                        if (DBG) Log.d(TAG, "read " + packet.getDataBuffer().length + " bytes");
311                        if (size < 0) {
312                            connectionBroken = true;
313                            break;
314                        } else {
315                            mRemoteSap = packet.getRemoteSap();
316                            echoMachine.pushUnit(dataUnit, size);
317                        }
318                    } catch (IOException e) {
319                        // Connection broken
320                        connectionBroken = true;
321                        if (DBG) Log.d(TAG, "connection broken by IOException", e);
322                    }
323                }
324            } catch (LlcpException e) {
325                Log.e(TAG, "llcp error", e);
326            } finally {
327                echoMachine.shutdown();
328
329                if (socket != null) {
330                    try {
331                        socket.close();
332                    } catch (IOException e) {
333                    }
334                }
335            }
336
337        }
338
339        public void shutdown() {
340            mRunning = false;
341        }
342
343        @Override
344        public void write(byte[] data) {
345            try {
346                socket.send(mRemoteSap, data);
347            } catch (IOException e) {
348                if (DBG) Log.d(TAG, "Error writing data.");
349            }
350        }
351    }
352
353    public void onLlcpActivated() {
354        synchronized (this) {
355            // Connectionless server can only be started once the link is up
356            // - otherwise, all calls to receive() on the connectionless socket
357            // will fail immediately.
358            if (mConnectionlessServerThread == null) {
359                mConnectionlessServerThread = new ConnectionlessServerThread();
360                mConnectionlessServerThread.start();
361            }
362        }
363    }
364
365    public void onLlcpDeactivated() {
366        synchronized (this) {
367            if (mConnectionlessServerThread != null) {
368                mConnectionlessServerThread.shutdown();
369                mConnectionlessServerThread = null;
370            }
371        }
372    }
373
374    /**
375     *  Needs to be called on the UI thread
376     */
377    public void start() {
378        synchronized (this) {
379            if (mServerThread == null) {
380                mServerThread = new ServerThread();
381                mServerThread.start();
382            }
383        }
384
385    }
386
387    public void stop() {
388        synchronized (this) {
389            if (mServerThread != null) {
390                mServerThread.shutdown();
391                mServerThread = null;
392            }
393        }
394    }
395}
396