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