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