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