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