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