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