WebRtcBluetoothManager.java 26 KB

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