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 
17 package com.android.nfc.echoserver;
18 
19 import com.android.nfc.DeviceHost.LlcpConnectionlessSocket;
20 import com.android.nfc.LlcpException;
21 import com.android.nfc.DeviceHost.LlcpServerSocket;
22 import com.android.nfc.DeviceHost.LlcpSocket;
23 import com.android.nfc.LlcpPacket;
24 import com.android.nfc.NfcService;
25 
26 import android.os.Handler;
27 import android.os.Message;
28 import android.util.Log;
29 
30 import java.io.IOException;
31 import java.util.concurrent.BlockingQueue;
32 import 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  */
59 public 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 {
write(byte[] data)77         public void write(byte[] data);
78     }
79 
EchoServer()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 
EchoMachine(WriteCallback callback, boolean dumpWhenFull)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 
pushUnit(byte[] unit, int size)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          */
shutdown()153         public synchronized void shutdown() {
154             dataQueue.clear();
155             shutdown = true;
156         }
157 
158         @Override
handleMessage(Message msg)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 
ServerThread()175         public ServerThread() {
176             super();
177             echoMachine = new EchoMachine(this, false);
178         }
179 
handleClient(LlcpSocket socket)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
run()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
write(byte[] data)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 
shutdown()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 
ConnectionlessServerThread()283         public ConnectionlessServerThread() {
284             super();
285             echoMachine = new EchoMachine(this, true);
286         }
287 
288         @Override
run()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 
shutdown()339         public void shutdown() {
340             mRunning = false;
341         }
342 
343         @Override
write(byte[] data)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 
onLlcpActivated()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 
onLlcpDeactivated()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      */
start()377     public void start() {
378         synchronized (this) {
379             if (mServerThread == null) {
380                 mServerThread = new ServerThread();
381                 mServerThread.start();
382             }
383         }
384 
385     }
386 
stop()387     public void stop() {
388         synchronized (this) {
389             if (mServerThread != null) {
390                 mServerThread.shutdown();
391                 mServerThread = null;
392             }
393         }
394     }
395 }
396