1 /* 2 * Copyright 2016 The WebRTC Project Authors. All rights reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 package org.appspot.apprtc; 12 13 import android.annotation.SuppressLint; 14 import android.bluetooth.BluetoothAdapter; 15 import android.bluetooth.BluetoothDevice; 16 import android.bluetooth.BluetoothHeadset; 17 import android.bluetooth.BluetoothProfile; 18 import android.content.BroadcastReceiver; 19 import android.content.Context; 20 import android.content.Intent; 21 import android.content.IntentFilter; 22 import android.content.pm.PackageManager; 23 import android.media.AudioManager; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.os.Process; 27 import android.support.annotation.Nullable; 28 import android.util.Log; 29 import java.util.List; 30 import java.util.Set; 31 import org.appspot.apprtc.util.AppRTCUtils; 32 import org.webrtc.ThreadUtils; 33 34 /** 35 * AppRTCProximitySensor manages functions related to Bluetoth devices in the 36 * AppRTC demo. 37 */ 38 public class AppRTCBluetoothManager { 39 private static final String TAG = "AppRTCBluetoothManager"; 40 41 // Timeout interval for starting or stopping audio to a Bluetooth SCO device. 42 private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; 43 // Maximum number of SCO connection attempts. 44 private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; 45 46 // Bluetooth connection state. 47 public enum State { 48 // Bluetooth is not available; no adapter or Bluetooth is off. 49 UNINITIALIZED, 50 // Bluetooth error happened when trying to start Bluetooth. 51 ERROR, 52 // Bluetooth proxy object for the Headset profile exists, but no connected headset devices, 53 // SCO is not started or disconnected. 54 HEADSET_UNAVAILABLE, 55 // Bluetooth proxy object for the Headset profile connected, connected Bluetooth headset 56 // present, but SCO is not started or disconnected. 57 HEADSET_AVAILABLE, 58 // Bluetooth audio SCO connection with remote device is closing. 59 SCO_DISCONNECTING, 60 // Bluetooth audio SCO connection with remote device is initiated. 61 SCO_CONNECTING, 62 // Bluetooth audio SCO connection with remote device is established. 63 SCO_CONNECTED 64 } 65 66 private final Context apprtcContext; 67 private final AppRTCAudioManager apprtcAudioManager; 68 @Nullable 69 private final AudioManager audioManager; 70 private final Handler handler; 71 72 int scoConnectionAttempts; 73 private State bluetoothState; 74 private final BluetoothProfile.ServiceListener bluetoothServiceListener; 75 @Nullable 76 private BluetoothAdapter bluetoothAdapter; 77 @Nullable 78 private BluetoothHeadset bluetoothHeadset; 79 @Nullable 80 private BluetoothDevice bluetoothDevice; 81 private final BroadcastReceiver bluetoothHeadsetReceiver; 82 83 // Runs when the Bluetooth timeout expires. We use that timeout after calling 84 // startScoAudio() or stopScoAudio() because we're not guaranteed to get a 85 // callback after those calls. 86 private final Runnable bluetoothTimeoutRunnable = new Runnable() { 87 @Override 88 public void run() { 89 bluetoothTimeout(); 90 } 91 }; 92 93 /** 94 * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been 95 * connected to or disconnected from the service. 96 */ 97 private class BluetoothServiceListener implements BluetoothProfile.ServiceListener { 98 @Override 99 // Called to notify the client when the proxy object has been connected to the service. 100 // Once we have the profile proxy object, we can use it to monitor the state of the 101 // connection and perform other operations that are relevant to the headset profile. onServiceConnected(int profile, BluetoothProfile proxy)102 public void onServiceConnected(int profile, BluetoothProfile proxy) { 103 if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { 104 return; 105 } 106 Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); 107 // Android only supports one connected Bluetooth Headset at a time. 108 bluetoothHeadset = (BluetoothHeadset) proxy; 109 updateAudioDeviceState(); 110 Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState); 111 } 112 113 @Override 114 /** Notifies the client when the proxy object has been disconnected from the service. */ onServiceDisconnected(int profile)115 public void onServiceDisconnected(int profile) { 116 if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { 117 return; 118 } 119 Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); 120 stopScoAudio(); 121 bluetoothHeadset = null; 122 bluetoothDevice = null; 123 bluetoothState = State.HEADSET_UNAVAILABLE; 124 updateAudioDeviceState(); 125 Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState); 126 } 127 } 128 129 // Intent broadcast receiver which handles changes in Bluetooth device availability. 130 // Detects headset changes and Bluetooth SCO state changes. 131 private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver { 132 @Override onReceive(Context context, Intent intent)133 public void onReceive(Context context, Intent intent) { 134 if (bluetoothState == State.UNINITIALIZED) { 135 return; 136 } 137 final String action = intent.getAction(); 138 // Change in connection state of the Headset profile. Note that the 139 // change does not tell us anything about whether we're streaming 140 // audio to BT over SCO. Typically received when user turns on a BT 141 // headset while audio is active using another audio device. 142 if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { 143 final int state = 144 intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); 145 Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " 146 + "a=ACTION_CONNECTION_STATE_CHANGED, " 147 + "s=" + stateToString(state) + ", " 148 + "sb=" + isInitialStickyBroadcast() + ", " 149 + "BT state: " + bluetoothState); 150 if (state == BluetoothHeadset.STATE_CONNECTED) { 151 scoConnectionAttempts = 0; 152 updateAudioDeviceState(); 153 } else if (state == BluetoothHeadset.STATE_CONNECTING) { 154 // No action needed. 155 } else if (state == BluetoothHeadset.STATE_DISCONNECTING) { 156 // No action needed. 157 } else if (state == BluetoothHeadset.STATE_DISCONNECTED) { 158 // Bluetooth is probably powered off during the call. 159 stopScoAudio(); 160 updateAudioDeviceState(); 161 } 162 // Change in the audio (SCO) connection state of the Headset profile. 163 // Typically received after call to startScoAudio() has finalized. 164 } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { 165 final int state = intent.getIntExtra( 166 BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); 167 Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " 168 + "a=ACTION_AUDIO_STATE_CHANGED, " 169 + "s=" + stateToString(state) + ", " 170 + "sb=" + isInitialStickyBroadcast() + ", " 171 + "BT state: " + bluetoothState); 172 if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { 173 cancelTimer(); 174 if (bluetoothState == State.SCO_CONNECTING) { 175 Log.d(TAG, "+++ Bluetooth audio SCO is now connected"); 176 bluetoothState = State.SCO_CONNECTED; 177 scoConnectionAttempts = 0; 178 updateAudioDeviceState(); 179 } else { 180 Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); 181 } 182 } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { 183 Log.d(TAG, "+++ Bluetooth audio SCO is now connecting..."); 184 } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { 185 Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected"); 186 if (isInitialStickyBroadcast()) { 187 Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); 188 return; 189 } 190 updateAudioDeviceState(); 191 } 192 } 193 Log.d(TAG, "onReceive done: BT state=" + bluetoothState); 194 } 195 } 196 197 /** Construction. */ create(Context context, AppRTCAudioManager audioManager)198 static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) { 199 Log.d(TAG, "create" + AppRTCUtils.getThreadInfo()); 200 return new AppRTCBluetoothManager(context, audioManager); 201 } 202 AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager)203 protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) { 204 Log.d(TAG, "ctor"); 205 ThreadUtils.checkIsOnMainThread(); 206 apprtcContext = context; 207 apprtcAudioManager = audioManager; 208 this.audioManager = getAudioManager(context); 209 bluetoothState = State.UNINITIALIZED; 210 bluetoothServiceListener = new BluetoothServiceListener(); 211 bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver(); 212 handler = new Handler(Looper.getMainLooper()); 213 } 214 215 /** Returns the internal state. */ getState()216 public State getState() { 217 ThreadUtils.checkIsOnMainThread(); 218 return bluetoothState; 219 } 220 221 /** 222 * Activates components required to detect Bluetooth devices and to enable 223 * BT SCO (audio is routed via BT SCO) for the headset profile. The end 224 * state will be HEADSET_UNAVAILABLE but a state machine has started which 225 * will start a state change sequence where the final outcome depends on 226 * if/when the BT headset is enabled. 227 * Example of state change sequence when start() is called while BT device 228 * is connected and enabled: 229 * UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE --> 230 * SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO. 231 * Note that the AppRTCAudioManager is also involved in driving this state 232 * change. 233 */ start()234 public void start() { 235 ThreadUtils.checkIsOnMainThread(); 236 Log.d(TAG, "start"); 237 if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) { 238 Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission"); 239 return; 240 } 241 if (bluetoothState != State.UNINITIALIZED) { 242 Log.w(TAG, "Invalid BT state"); 243 return; 244 } 245 bluetoothHeadset = null; 246 bluetoothDevice = null; 247 scoConnectionAttempts = 0; 248 // Get a handle to the default local Bluetooth adapter. 249 bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 250 if (bluetoothAdapter == null) { 251 Log.w(TAG, "Device does not support Bluetooth"); 252 return; 253 } 254 // Ensure that the device supports use of BT SCO audio for off call use cases. 255 if (!audioManager.isBluetoothScoAvailableOffCall()) { 256 Log.e(TAG, "Bluetooth SCO audio is not available off call"); 257 return; 258 } 259 logBluetoothAdapterInfo(bluetoothAdapter); 260 // Establish a connection to the HEADSET profile (includes both Bluetooth Headset and 261 // Hands-Free) proxy object and install a listener. 262 if (!getBluetoothProfileProxy( 263 apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) { 264 Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed"); 265 return; 266 } 267 // Register receivers for BluetoothHeadset change notifications. 268 IntentFilter bluetoothHeadsetFilter = new IntentFilter(); 269 // Register receiver for change in connection state of the Headset profile. 270 bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 271 // Register receiver for change in audio connection state of the Headset profile. 272 bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); 273 registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter); 274 Log.d(TAG, "HEADSET profile state: " 275 + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET))); 276 Log.d(TAG, "Bluetooth proxy for headset profile has started"); 277 bluetoothState = State.HEADSET_UNAVAILABLE; 278 Log.d(TAG, "start done: BT state=" + bluetoothState); 279 } 280 281 /** Stops and closes all components related to Bluetooth audio. */ stop()282 public void stop() { 283 ThreadUtils.checkIsOnMainThread(); 284 Log.d(TAG, "stop: BT state=" + bluetoothState); 285 if (bluetoothAdapter == null) { 286 return; 287 } 288 // Stop BT SCO connection with remote device if needed. 289 stopScoAudio(); 290 // Close down remaining BT resources. 291 if (bluetoothState == State.UNINITIALIZED) { 292 return; 293 } 294 unregisterReceiver(bluetoothHeadsetReceiver); 295 cancelTimer(); 296 if (bluetoothHeadset != null) { 297 bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset); 298 bluetoothHeadset = null; 299 } 300 bluetoothAdapter = null; 301 bluetoothDevice = null; 302 bluetoothState = State.UNINITIALIZED; 303 Log.d(TAG, "stop done: BT state=" + bluetoothState); 304 } 305 306 /** 307 * Starts Bluetooth SCO connection with remote device. 308 * Note that the phone application always has the priority on the usage of the SCO connection 309 * for telephony. If this method is called while the phone is in call it will be ignored. 310 * Similarly, if a call is received or sent while an application is using the SCO connection, 311 * the connection will be lost for the application and NOT returned automatically when the call 312 * ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a 313 * virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO 314 * audio connection is established. 315 * TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and 316 * higher. It might be required to initiates a virtual voice call since many devices do not 317 * accept SCO audio without a "call". 318 */ startScoAudio()319 public boolean startScoAudio() { 320 ThreadUtils.checkIsOnMainThread(); 321 Log.d(TAG, "startSco: BT state=" + bluetoothState + ", " 322 + "attempts: " + scoConnectionAttempts + ", " 323 + "SCO is on: " + isScoOn()); 324 if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) { 325 Log.e(TAG, "BT SCO connection fails - no more attempts"); 326 return false; 327 } 328 if (bluetoothState != State.HEADSET_AVAILABLE) { 329 Log.e(TAG, "BT SCO connection fails - no headset available"); 330 return false; 331 } 332 // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED. 333 Log.d(TAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED..."); 334 // The SCO connection establishment can take several seconds, hence we cannot rely on the 335 // connection to be available when the method returns but instead register to receive the 336 // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED. 337 bluetoothState = State.SCO_CONNECTING; 338 audioManager.startBluetoothSco(); 339 audioManager.setBluetoothScoOn(true); 340 scoConnectionAttempts++; 341 startTimer(); 342 Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", " 343 + "SCO is on: " + isScoOn()); 344 return true; 345 } 346 347 /** Stops Bluetooth SCO connection with remote device. */ stopScoAudio()348 public void stopScoAudio() { 349 ThreadUtils.checkIsOnMainThread(); 350 Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " 351 + "SCO is on: " + isScoOn()); 352 if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) { 353 return; 354 } 355 cancelTimer(); 356 audioManager.stopBluetoothSco(); 357 audioManager.setBluetoothScoOn(false); 358 bluetoothState = State.SCO_DISCONNECTING; 359 Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", " 360 + "SCO is on: " + isScoOn()); 361 } 362 363 /** 364 * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset 365 * Service via IPC) to update the list of connected devices for the HEADSET 366 * profile. The internal state will change to HEADSET_UNAVAILABLE or to 367 * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected 368 * device if available. 369 */ updateDevice()370 public void updateDevice() { 371 if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { 372 return; 373 } 374 Log.d(TAG, "updateDevice"); 375 // Get connected devices for the headset profile. Returns the set of 376 // devices which are in state STATE_CONNECTED. The BluetoothDevice class 377 // is just a thin wrapper for a Bluetooth hardware address. 378 List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices(); 379 if (devices.isEmpty()) { 380 bluetoothDevice = null; 381 bluetoothState = State.HEADSET_UNAVAILABLE; 382 Log.d(TAG, "No connected bluetooth headset"); 383 } else { 384 // Always use first device in list. Android only supports one device. 385 bluetoothDevice = devices.get(0); 386 bluetoothState = State.HEADSET_AVAILABLE; 387 Log.d(TAG, "Connected bluetooth headset: " 388 + "name=" + bluetoothDevice.getName() + ", " 389 + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) 390 + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice)); 391 } 392 Log.d(TAG, "updateDevice done: BT state=" + bluetoothState); 393 } 394 395 /** 396 * Stubs for test mocks. 397 */ 398 @Nullable getAudioManager(Context context)399 protected AudioManager getAudioManager(Context context) { 400 return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 401 } 402 registerReceiver(BroadcastReceiver receiver, IntentFilter filter)403 protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { 404 apprtcContext.registerReceiver(receiver, filter); 405 } 406 unregisterReceiver(BroadcastReceiver receiver)407 protected void unregisterReceiver(BroadcastReceiver receiver) { 408 apprtcContext.unregisterReceiver(receiver); 409 } 410 getBluetoothProfileProxy( Context context, BluetoothProfile.ServiceListener listener, int profile)411 protected boolean getBluetoothProfileProxy( 412 Context context, BluetoothProfile.ServiceListener listener, int profile) { 413 return bluetoothAdapter.getProfileProxy(context, listener, profile); 414 } 415 hasPermission(Context context, String permission)416 protected boolean hasPermission(Context context, String permission) { 417 return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid()) 418 == PackageManager.PERMISSION_GRANTED; 419 } 420 421 /** Logs the state of the local Bluetooth adapter. */ 422 @SuppressLint("HardwareIds") logBluetoothAdapterInfo(BluetoothAdapter localAdapter)423 protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { 424 Log.d(TAG, "BluetoothAdapter: " 425 + "enabled=" + localAdapter.isEnabled() + ", " 426 + "state=" + stateToString(localAdapter.getState()) + ", " 427 + "name=" + localAdapter.getName() + ", " 428 + "address=" + localAdapter.getAddress()); 429 // Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter. 430 Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices(); 431 if (!pairedDevices.isEmpty()) { 432 Log.d(TAG, "paired devices:"); 433 for (BluetoothDevice device : pairedDevices) { 434 Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress()); 435 } 436 } 437 } 438 439 /** Ensures that the audio manager updates its list of available audio devices. */ updateAudioDeviceState()440 private void updateAudioDeviceState() { 441 ThreadUtils.checkIsOnMainThread(); 442 Log.d(TAG, "updateAudioDeviceState"); 443 apprtcAudioManager.updateAudioDeviceState(); 444 } 445 446 /** Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. */ startTimer()447 private void startTimer() { 448 ThreadUtils.checkIsOnMainThread(); 449 Log.d(TAG, "startTimer"); 450 handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS); 451 } 452 453 /** Cancels any outstanding timer tasks. */ cancelTimer()454 private void cancelTimer() { 455 ThreadUtils.checkIsOnMainThread(); 456 Log.d(TAG, "cancelTimer"); 457 handler.removeCallbacks(bluetoothTimeoutRunnable); 458 } 459 460 /** 461 * Called when start of the BT SCO channel takes too long time. Usually 462 * happens when the BT device has been turned on during an ongoing call. 463 */ bluetoothTimeout()464 private void bluetoothTimeout() { 465 ThreadUtils.checkIsOnMainThread(); 466 if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { 467 return; 468 } 469 Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " 470 + "attempts: " + scoConnectionAttempts + ", " 471 + "SCO is on: " + isScoOn()); 472 if (bluetoothState != State.SCO_CONNECTING) { 473 return; 474 } 475 // Bluetooth SCO should be connecting; check the latest result. 476 boolean scoConnected = false; 477 List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices(); 478 if (devices.size() > 0) { 479 bluetoothDevice = devices.get(0); 480 if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) { 481 Log.d(TAG, "SCO connected with " + bluetoothDevice.getName()); 482 scoConnected = true; 483 } else { 484 Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName()); 485 } 486 } 487 if (scoConnected) { 488 // We thought BT had timed out, but it's actually on; updating state. 489 bluetoothState = State.SCO_CONNECTED; 490 scoConnectionAttempts = 0; 491 } else { 492 // Give up and "cancel" our request by calling stopBluetoothSco(). 493 Log.w(TAG, "BT failed to connect after timeout"); 494 stopScoAudio(); 495 } 496 updateAudioDeviceState(); 497 Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState); 498 } 499 500 /** Checks whether audio uses Bluetooth SCO. */ isScoOn()501 private boolean isScoOn() { 502 return audioManager.isBluetoothScoOn(); 503 } 504 505 /** Converts BluetoothAdapter states into local string representations. */ stateToString(int state)506 private String stateToString(int state) { 507 switch (state) { 508 case BluetoothAdapter.STATE_DISCONNECTED: 509 return "DISCONNECTED"; 510 case BluetoothAdapter.STATE_CONNECTED: 511 return "CONNECTED"; 512 case BluetoothAdapter.STATE_CONNECTING: 513 return "CONNECTING"; 514 case BluetoothAdapter.STATE_DISCONNECTING: 515 return "DISCONNECTING"; 516 case BluetoothAdapter.STATE_OFF: 517 return "OFF"; 518 case BluetoothAdapter.STATE_ON: 519 return "ON"; 520 case BluetoothAdapter.STATE_TURNING_OFF: 521 // Indicates the local Bluetooth adapter is turning off. Local clients should immediately 522 // attempt graceful disconnection of any remote links. 523 return "TURNING_OFF"; 524 case BluetoothAdapter.STATE_TURNING_ON: 525 // Indicates the local Bluetooth adapter is turning on. However local clients should wait 526 // for STATE_ON before attempting to use the adapter. 527 return "TURNING_ON"; 528 default: 529 return "INVALID"; 530 } 531 } 532 } 533