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