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}