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