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