AppRTCBluetoothManager.java

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