1 /*
2  * Copyright (C) 2011 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.cts.verifier.bluetooth;
18 
19 import static org.junit.Assert.assertTrue;
20 
21 import android.annotation.NonNull;
22 import android.bluetooth.BluetoothAdapter;
23 import android.bluetooth.BluetoothDevice;
24 import android.bluetooth.BluetoothServerSocket;
25 import android.bluetooth.BluetoothSocket;
26 import android.content.Context;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.Message;
30 import android.util.Log;
31 
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.io.OutputStream;
35 import java.util.UUID;
36 
37 /**
38  * This class does all the work for setting up and managing Bluetooth
39  * connections with other devices. It has a thread that listens for
40  * incoming connections, a thread for connecting with a device, and a
41  * thread for performing data transmissions when connected.
42  */
43 public class BluetoothChatService {
44     // Message types sent from the BluetoothChatService Handler
45     public static final int MESSAGE_STATE_CHANGE = 1;
46     public static final int MESSAGE_READ = 2;
47     public static final int MESSAGE_WRITE = 3;
48     public static final int MESSAGE_DEVICE_NAME = 4;
49     public static final int MESSAGE_TOAST = 5;
50 
51     // Key names received from the BluetoothChatService Handler
52     public static final String DEVICE_NAME = "device_name";
53     public static final String TOAST = "toast";
54 
55     static final UUID SECURE_UUID =
56             UUID.fromString("8591d757-18ee-45e1-9b12-92875d06ba23");
57     static final UUID INSECURE_UUID =
58             UUID.fromString("301c214f-91a2-43bf-a795-09d1198a81a7");
59     static final UUID HANDSFREE_INSECURE_UUID =
60             UUID.fromString("0000111F-0000-1000-8000-00805F9B34FB");
61 
62     // Debugging
63     private static final String TAG = "CtsBluetoothChatService";
64     private static final boolean D = true;
65 
66     // Name for the SDP record when creating server socket
67     private static final String NAME_SECURE = "CtsBluetoothChatSecure";
68     private static final String NAME_INSECURE = "CtsBluetoothChatInsecure";
69 
70     // Member fields
71     private final BluetoothAdapter mAdapter;
72     private final Handler mHandler;
73     private final UUID mUuid;
74     private AcceptThread mSecureAcceptThread;
75     private AcceptThread mInsecureAcceptThread;
76     private ConnectThread mConnectThread;
77     private ConnectedThread mConnectedThread;
78     private int mState;
79     private boolean mBleTransport;
80     private int mLePsm;
81     private int mSocketConnectionType = -1;
82 
83     // Constants that indicate the current connection state
84     public static final int STATE_NONE = 0;       // we're doing nothing
85     public static final int STATE_LISTEN = 1;     // now listening for incoming connections
86     public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
87     public static final int STATE_CONNECTED = 3;  // now connected to a remote device
88 
89     /**
90      * Constructor. Prepares a new BluetoothChat session.
91      * @param context  The UI Activity Context
92      * @param handler  A Handler to send messages back to the UI Activity
93      */
BluetoothChatService(Context context, Handler handler, UUID uuid)94     public BluetoothChatService(Context context, Handler handler, UUID uuid) {
95         mAdapter = BluetoothAdapter.getDefaultAdapter();
96         mState = STATE_NONE;
97         mHandler = handler;
98         mUuid = uuid;
99         mBleTransport = false;
100     }
101 
102     /**
103      * Constructor. Prepares a new BluetoothChat session.
104      * @param context  The UI Activity Context
105      * @param handler  A Handler to send messages back to the UI Activity
106      * @param useBle   A flag to use the BLE transport
107      */
BluetoothChatService(Context context, Handler handler, boolean useBle)108     public BluetoothChatService(Context context, Handler handler, boolean useBle) {
109         mAdapter = BluetoothAdapter.getDefaultAdapter();
110         mState = STATE_NONE;
111         mHandler = handler;
112         mUuid = null;
113         mBleTransport = useBle;
114         if (D) Log.d(TAG, "Construct BluetoothChatService: useBle=" + useBle);
115     }
116 
117     /**
118      * Set the current state of the chat connection
119      * @param state  An integer defining the current connection state
120      */
setState(int state)121     private synchronized void setState(int state) {
122         if (D) Log.d(TAG, "setState() " + mState + " -> " + state);
123         mState = state;
124 
125         // Give the new state to the Handler so the UI Activity can update
126         mHandler.obtainMessage(MESSAGE_STATE_CHANGE, state, -1).sendToTarget();
127     }
128 
129     /**
130      * Return the current connection state. */
getState()131     public synchronized int getState() {
132         return mState;
133     }
134 
135     /**
136      * Start the chat service. Specifically start AcceptThread to begin a
137      * session in listening (server) mode. Called by the Activity onResume() */
start(boolean secure)138     public synchronized void start(boolean secure) {
139         if (D) Log.d(TAG, "start secure: " + secure + UUID.randomUUID() + " - " + UUID.randomUUID());
140 
141         // Cancel any thread attempting to make a connection
142         if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
143 
144         // Cancel any thread currently running a connection
145         if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
146 
147         setState(STATE_LISTEN);
148 
149         // Start the thread to listen on a BluetoothServerSocket
150         if (secure && mSecureAcceptThread == null) {
151             mSecureAcceptThread = new AcceptThread(true);
152             mSecureAcceptThread.start();
153         }
154         if (!secure && mInsecureAcceptThread == null) {
155             mInsecureAcceptThread = new AcceptThread(false);
156             mInsecureAcceptThread.start();
157         }
158     }
159 
160     /**
161      * Return the assigned PSM value.
162      */
getPsm(boolean secure)163     public synchronized int getPsm(boolean secure) {
164         if (secure && mSecureAcceptThread != null) {
165             return mSecureAcceptThread.getPsm();
166         }
167         else if (!secure && mInsecureAcceptThread != null) {
168             return mInsecureAcceptThread.getPsm();
169         }
170         Log.e(TAG, "getPsm: Invalid PSM value");
171         return 0;
172     }
173 
174     /**
175      * Return the socket Connection Type.
176      */
getSocketConnectionType()177     public synchronized int getSocketConnectionType() {
178         return mSocketConnectionType;
179     }
180 
181     /**
182      * Start the ConnectThread to initiate a connection to a remote device.
183      * @param device  The BluetoothDevice to connect to
184      * @param secure Socket Security type - Secure (true) , Insecure (false)
185      */
connect(BluetoothDevice device, boolean secure)186     public synchronized void connect(BluetoothDevice device, boolean secure) {
187         if (!mBleTransport) {
188             connect(device, secure, 0);
189         } else {
190             Log.e(TAG, "connect: Error: LE cannot call this method!");
191         }
192     }
193 
194     /**
195      * Start the ConnectThread to initiate a connection to a remote device.
196      * @param device  The BluetoothDevice to connect to
197      * @param secure Socket Security type - Secure (true) , Insecure (false)
198      * @param psm Assigned PSM value
199      */
connect(BluetoothDevice device, boolean secure, int psm)200     public synchronized void connect(BluetoothDevice device, boolean secure, int psm) {
201         if (D) Log.d(TAG, "connect to: " + device + ", psm: " + psm + ", ble: " + mBleTransport);
202 
203         // Cancel any thread attempting to make a connection
204         if (mState == STATE_CONNECTING) {
205             if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
206         }
207 
208         // Cancel any thread currently running a connection
209         if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
210 
211         // Start the thread to connect with the given device
212         mConnectThread = new ConnectThread(device, secure, psm);
213         mConnectThread.start();
214         setState(STATE_CONNECTING);
215     }
216 
217     /**
218      * Start the ConnectedThread to begin managing a Bluetooth connection
219      * @param socket  The BluetoothSocket on which the connection was made
220      * @param device  The BluetoothDevice that has been connected
221      */
connected(@onNull BluetoothSocket socket, BluetoothDevice device, final String socketType)222     public synchronized void connected(@NonNull BluetoothSocket socket, BluetoothDevice
223             device, final String socketType) {
224         if (D) {
225             Log.d(TAG, "connected, Socket Type: " + socketType
226                     + ", MaxReceivePacketSize: " + socket.getMaxReceivePacketSize()
227                     + ", MaxTransmitPacketSize: " + socket.getMaxTransmitPacketSize());
228         }
229         assertTrue("socket.getMaxReceivePacketSize() expected to be non negative value instead of "
230                 + socket.getMaxReceivePacketSize(), socket.getMaxReceivePacketSize() >= 0);
231         assertTrue("socket.getMaxTransmitPacketSize() expected to be non negative value instead of "
232                 + socket.getMaxTransmitPacketSize(), socket.getMaxTransmitPacketSize() >= 0);
233 
234         // Cancel the thread that completed the connection
235         if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
236 
237         // Cancel any thread currently running a connection
238         if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
239 
240         // Cancel the accept thread because we only want to connect to one device
241         if (mSecureAcceptThread != null) {
242             mSecureAcceptThread.cancel();
243             mSecureAcceptThread = null;
244         }
245         if (mInsecureAcceptThread != null) {
246             mInsecureAcceptThread.cancel();
247             mInsecureAcceptThread = null;
248         }
249 
250         // Start the thread to manage the connection and perform transmissions
251         mConnectedThread = new ConnectedThread(socket, socketType);
252         mConnectedThread.start();
253 
254         // Send the name of the connected device back to the UI Activity
255         Message msg = mHandler.obtainMessage(MESSAGE_DEVICE_NAME);
256         Bundle bundle = new Bundle();
257         bundle.putString(DEVICE_NAME, device.getName());
258         msg.setData(bundle);
259         mHandler.sendMessage(msg);
260 
261         setState(STATE_CONNECTED);
262     }
263 
264     /**
265      * Stop all threads
266      */
stop()267     public synchronized void stop() {
268         if (D) Log.d(TAG, "stop");
269 
270         if (mConnectThread != null) {
271             mConnectThread.cancel();
272             mConnectThread = null;
273         }
274 
275         if (mConnectedThread != null) {
276             mConnectedThread.cancel();
277             mConnectedThread = null;
278         }
279 
280         if (mSecureAcceptThread != null) {
281             mSecureAcceptThread.cancel();
282             mSecureAcceptThread = null;
283         }
284 
285         if (mInsecureAcceptThread != null) {
286             mInsecureAcceptThread.cancel();
287             mInsecureAcceptThread = null;
288         }
289         setState(STATE_NONE);
290     }
291 
292     /**
293      * Write to the ConnectedThread in an unsynchronized manner
294      * @param out The bytes to write
295      * @see ConnectedThread#write(byte[])
296      */
write(byte[] out)297     public void write(byte[] out) {
298         // Create temporary object
299         ConnectedThread r;
300         // Synchronize a copy of the ConnectedThread
301         synchronized (this) {
302             if (mState != STATE_CONNECTED) return;
303             r = mConnectedThread;
304         }
305         // Perform the write unsynchronized
306         r.write(out);
307     }
308 
309     /**
310      * Indicate that the connection attempt failed and notify the UI Activity.
311      */
connectionFailed()312     private void connectionFailed() {
313         // Send a failure message back to the Activity
314         Message msg = mHandler.obtainMessage(MESSAGE_TOAST);
315         Bundle bundle = new Bundle();
316         bundle.putString(TOAST, "Unable to connect device");
317         msg.setData(bundle);
318         mHandler.sendMessage(msg);
319     }
320 
321     /**
322      * Indicate that the connection was lost and notify the UI Activity.
323      */
connectionLost()324     private void connectionLost() {
325         // Send a failure message back to the Activity
326         Message msg = mHandler.obtainMessage(MESSAGE_TOAST);
327         Bundle bundle = new Bundle();
328         bundle.putString(TOAST, "Device connection was lost");
329         msg.setData(bundle);
330         mHandler.sendMessage(msg);
331     }
332 
333     /**
334      * This thread runs while listening for incoming connections. It behaves
335      * like a server-side client. It runs until a connection is accepted
336      * (or until cancelled).
337      */
338     private class AcceptThread extends Thread {
339         // The local server socket
340         private final BluetoothServerSocket mmServerSocket;
341         private String mSocketType;
342 
AcceptThread(boolean secure)343         public AcceptThread(boolean secure) {
344             BluetoothServerSocket tmp = null;
345             mSocketType = secure ? "Secure" : "Insecure";
346 
347             // Create a new listening server socket
348             try {
349                 if (mBleTransport) {
350                     if (secure) {
351                         tmp = mAdapter.listenUsingL2capChannel();
352                     } else {
353                         tmp = mAdapter.listenUsingInsecureL2capChannel();
354                     }
355                 } else {
356                     if (secure) {
357                         tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME_SECURE, mUuid);
358                     } else {
359                         tmp = mAdapter.listenUsingInsecureRfcommWithServiceRecord(NAME_INSECURE, mUuid);
360                     }
361                 }
362             } catch (IOException e) {
363                 Log.e(TAG, "Socket Type: " + mSocketType + ", le: " + mBleTransport + " listen() failed", e);
364             }
365             mmServerSocket = tmp;
366             if (mBleTransport) {
367                 // Get the assigned PSM value
368                 mLePsm = mmServerSocket.getPsm();
369             }
370         }
371 
getPsm()372         public int getPsm() {
373             return mLePsm;
374         }
375 
run()376         public void run() {
377             if (D) Log.d(TAG, "Socket Type: " + mSocketType +
378                     " BEGIN mAcceptThread" + this);
379             setName("AcceptThread" + mSocketType);
380 
381             BluetoothSocket socket = null;
382 
383             // Listen to the server socket if we're not connected
384             while (mState != STATE_CONNECTED) {
385                 try {
386                     // This is a blocking call and will only return on a
387                     // successful connection or an exception
388                     socket = mmServerSocket.accept();
389                 } catch (IOException e) {
390                     Log.e(TAG, "Socket Type: " + mSocketType + " accept() failed", e);
391                     break;
392                 }
393 
394                 // If a connection was accepted
395                 if (socket != null) {
396                     synchronized (BluetoothChatService.this) {
397                         switch (mState) {
398                         case STATE_LISTEN:
399                         case STATE_CONNECTING:
400                             // Situation normal. Start the connected thread.
401                             mSocketConnectionType = socket.getConnectionType();
402                             connected(socket, socket.getRemoteDevice(),
403                                     mSocketType);
404                             break;
405                         case STATE_NONE:
406                         case STATE_CONNECTED:
407                             // Either not ready or already connected. Terminate new socket.
408                             try {
409                                 socket.close();
410                             } catch (IOException e) {
411                                 Log.e(TAG, "Could not close unwanted socket", e);
412                             }
413                             break;
414                         }
415                     }
416                 } else {
417                     Log.i(TAG, "Got null socket");
418                 }
419             }
420             if (D) {
421                 Log.i(TAG, "END mAcceptThread, socket Type: " + mSocketType
422                          + ", SocketConnectionType: " + mSocketConnectionType);
423             }
424         }
425 
cancel()426         public void cancel() {
427             if (D) Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this);
428             try {
429                 mmServerSocket.close();
430             } catch (IOException e) {
431                 Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e);
432             }
433         }
434     }
435 
436 
437     /**
438      * This thread runs while attempting to make an outgoing connection
439      * with a device. It runs straight through; the connection either
440      * succeeds or fails.
441      */
442     private class ConnectThread extends Thread {
443         private final BluetoothSocket mmSocket;
444         private final BluetoothDevice mmDevice;
445         private String mSocketType;
446 
ConnectThread(BluetoothDevice device, boolean secure)447         public ConnectThread(BluetoothDevice device, boolean secure) {
448             if (mBleTransport) {
449                 Log.e(TAG, "ConnectThread: Error: LE should not call this constructor");
450             }
451             mmDevice = device;
452             mmSocket = connectThreadCommon(device, secure, 0);
453         }
454 
ConnectThread(BluetoothDevice device, boolean secure, int psm)455         public ConnectThread(BluetoothDevice device, boolean secure, int psm) {
456             mmDevice = device;
457             mmSocket = connectThreadCommon(device, secure, psm);
458         }
459 
connectThreadCommon(BluetoothDevice device, boolean secure, int psm)460         private BluetoothSocket connectThreadCommon(BluetoothDevice device, boolean secure, int psm) {
461             BluetoothSocket tmp = null;
462             mSocketType = secure ? "Secure" : "Insecure";
463 
464             // Get a BluetoothSocket for a connection with the
465             // given BluetoothDevice
466             try {
467                 if (mBleTransport) {
468                     if (secure) {
469                         tmp = device.createL2capChannel(psm);
470                     } else {
471                         tmp = device.createInsecureL2capChannel(psm);
472                     }
473                 } else {
474                     if (secure) {
475                         tmp = device.createRfcommSocketToServiceRecord(mUuid);
476                     } else {
477                         tmp = device.createInsecureRfcommSocketToServiceRecord(mUuid);
478                     }
479                 }
480             } catch (IOException e) {
481                 Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e);
482             }
483 
484             mSocketConnectionType = tmp.getConnectionType();
485 
486             return tmp;
487         }
488 
run()489         public void run() {
490             Log.i(TAG, "BEGIN mConnectThread SocketType:" + mSocketType
491                   + ", mSocketConnectionType: " + mSocketConnectionType);
492             setName("ConnectThread" + mSocketType);
493 
494             // Always cancel discovery because it will slow down a connection
495             mAdapter.cancelDiscovery();
496 
497             // Make a connection to the BluetoothSocket
498             try {
499                 // This is a blocking call and will only return on a
500                 // successful connection or an exception
501                 mmSocket.connect();
502             } catch (IOException e) {
503                 Log.e(TAG, "connect() failed ", e);
504                 // Close the socket
505                 try {
506                     mmSocket.close();
507                 } catch (IOException e2) {
508                     Log.e(TAG, "unable to close() " + mSocketType +
509                             " socket during connection failure", e2);
510                 }
511                 connectionFailed();
512                 return;
513             }
514 
515             // Reset the ConnectThread because we're done
516             synchronized (BluetoothChatService.this) {
517                 mConnectThread = null;
518             }
519 
520             // Start the connected thread
521             connected(mmSocket, mmDevice, mSocketType);
522         }
523 
cancel()524         public void cancel() {
525             try {
526                 mmSocket.close();
527             } catch (IOException e) {
528                 Log.e(TAG, "close() of connect " + mSocketType + " socket failed", e);
529             }
530         }
531     }
532 
533     /**
534      * This thread runs during a connection with a remote device.
535      * It handles all incoming and outgoing transmissions.
536      */
537     private class ConnectedThread extends Thread {
538         private final BluetoothSocket mmSocket;
539         private final InputStream mmInStream;
540         private final OutputStream mmOutStream;
541 
ConnectedThread(BluetoothSocket socket, String socketType)542         public ConnectedThread(BluetoothSocket socket, String socketType) {
543             Log.d(TAG, "create ConnectedThread: " + socketType);
544             mmSocket = socket;
545             InputStream tmpIn = null;
546             OutputStream tmpOut = null;
547 
548             // Get the BluetoothSocket input and output streams
549             try {
550                 tmpIn = socket.getInputStream();
551                 tmpOut = socket.getOutputStream();
552             } catch (IOException e) {
553                 Log.e(TAG, "temp sockets not created", e);
554             }
555 
556             mmInStream = tmpIn;
557             mmOutStream = tmpOut;
558         }
559 
run()560         public void run() {
561             Log.i(TAG, "BEGIN mConnectedThread");
562             int bytes;
563 
564             // Keep listening to the InputStream while connected
565             while (true) {
566                 try {
567                     byte[] buffer = new byte[1024];
568                     // Read from the InputStream
569                     bytes = mmInStream.read(buffer);
570 
571                     // Send the obtained bytes to the UI Activity
572                     mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
573                             .sendToTarget();
574                 } catch (IOException e) {
575                     Log.e(TAG, "disconnected", e);
576                     connectionLost();
577                     break;
578                 }
579             }
580         }
581 
582         /**
583          * Write to the connected OutStream.
584          * @param buffer  The bytes to write
585          */
write(byte[] buffer)586         public void write(byte[] buffer) {
587             try {
588                 mmOutStream.write(buffer);
589                 mmOutStream.flush();
590 
591                 // Share the sent message back to the UI Activity
592                 mHandler.obtainMessage(MESSAGE_WRITE, -1, -1, buffer)
593                         .sendToTarget();
594             } catch (IOException e) {
595                 Log.e(TAG, "Exception during write", e);
596             }
597         }
598 
cancel()599         public void cancel() {
600             try {
601                 mmSocket.close();
602             } catch (IOException e) {
603                 Log.e(TAG, "close() of connect socket failed", e);
604             }
605         }
606     }
607 }
608