MagicBluetoothManager.java 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. *
  20. * Original code:
  21. *
  22. *
  23. * Copyright 2016 The WebRTC Project Authors. All rights reserved.
  24. *
  25. * Use of this source code is governed by a BSD-style license
  26. * that can be found in the LICENSE file in the root of the source
  27. * tree. An additional intellectual property rights grant can be found
  28. * in the file PATENTS. All contributing project authors may
  29. * be found in the AUTHORS file in the root of the source tree.
  30. */
  31. package com.nextcloud.talk.webrtc;
  32. import android.Manifest;
  33. import android.annotation.SuppressLint;
  34. import android.bluetooth.BluetoothAdapter;
  35. import android.bluetooth.BluetoothDevice;
  36. import android.bluetooth.BluetoothHeadset;
  37. import android.bluetooth.BluetoothProfile;
  38. import android.content.BroadcastReceiver;
  39. import android.content.Context;
  40. import android.content.Intent;
  41. import android.content.IntentFilter;
  42. import android.content.pm.PackageManager;
  43. import android.media.AudioManager;
  44. import android.os.Build;
  45. import android.os.Handler;
  46. import android.os.Looper;
  47. import android.os.Process;
  48. import android.util.Log;
  49. import org.webrtc.ThreadUtils;
  50. import java.util.List;
  51. import java.util.Set;
  52. import androidx.core.app.ActivityCompat;
  53. public class MagicBluetoothManager {
  54. private static final String TAG = "MagicBluetoothManager";
  55. // Timeout interval for starting or stopping audio to a Bluetooth SCO device.
  56. private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000;
  57. // Maximum number of SCO connection attempts.
  58. private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2;
  59. private final Context apprtcContext;
  60. private final MagicAudioManager apprtcAudioManager;
  61. private final AudioManager audioManager;
  62. private final Handler handler;
  63. private final BluetoothProfile.ServiceListener bluetoothServiceListener;
  64. private final BroadcastReceiver bluetoothHeadsetReceiver;
  65. int scoConnectionAttempts;
  66. private State bluetoothState;
  67. private BluetoothAdapter bluetoothAdapter;
  68. private BluetoothHeadset bluetoothHeadset;
  69. private BluetoothDevice bluetoothDevice;
  70. // Runs when the Bluetooth timeout expires. We use that timeout after calling
  71. // startScoAudio() or stopScoAudio() because we're not guaranteed to get a
  72. // callback after those calls.
  73. private final Runnable bluetoothTimeoutRunnable = this::bluetoothTimeout;
  74. protected MagicBluetoothManager(Context context, MagicAudioManager audioManager) {
  75. Log.d(TAG, "ctor");
  76. ThreadUtils.checkIsOnMainThread();
  77. apprtcContext = context;
  78. apprtcAudioManager = audioManager;
  79. this.audioManager = getAudioManager(context);
  80. bluetoothState = State.UNINITIALIZED;
  81. bluetoothServiceListener = new BluetoothServiceListener();
  82. bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
  83. handler = new Handler(Looper.getMainLooper());
  84. }
  85. /**
  86. * Construction.
  87. */
  88. static MagicBluetoothManager create(Context context, MagicAudioManager audioManager) {
  89. return new MagicBluetoothManager(context, audioManager);
  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 MagicAudioManager is also involved in driving this state
  110. * change.
  111. */
  112. @SuppressLint("MissingPermission")
  113. public void start() {
  114. ThreadUtils.checkIsOnMainThread();
  115. Log.d(TAG, "start");
  116. if(hasNoBluetoothPermission()){
  117. return;
  118. }
  119. if (bluetoothState != State.UNINITIALIZED) {
  120. Log.w(TAG, "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(TAG, "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(TAG, "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(TAG, "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(TAG, "HEADSET profile state: "
  153. + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
  154. Log.d(TAG, "Bluetooth proxy for headset profile has started");
  155. bluetoothState = State.HEADSET_UNAVAILABLE;
  156. Log.d(TAG, "start done: BT state=" + bluetoothState);
  157. }
  158. /**
  159. * Stops and closes all components related to Bluetooth audio.
  160. */
  161. public void stop() {
  162. ThreadUtils.checkIsOnMainThread();
  163. Log.d(TAG, "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(TAG, "stop done: BT state=" + bluetoothState);
  183. }
  184. /**
  185. * Starts Bluetooth SCO connection with remote device.
  186. * Note that the phone application always has the priority on the usage of the SCO connection
  187. * for telephony. If this method is called while the phone is in call it will be ignored.
  188. * Similarly, if a call is received or sent while an application is using the SCO connection,
  189. * the connection will be lost for the application and NOT returned automatically when the call
  190. * ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a
  191. * virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO
  192. * audio connection is established.
  193. * TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and
  194. * higher. It might be required to initiates a virtual voice call since many devices do not
  195. * accept SCO audio without a "call".
  196. */
  197. public boolean startScoAudio() {
  198. ThreadUtils.checkIsOnMainThread();
  199. Log.d(TAG, "startSco: BT state=" + bluetoothState + ", "
  200. + "attempts: " + scoConnectionAttempts + ", "
  201. + "SCO is on: " + isScoOn());
  202. if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
  203. Log.e(TAG, "BT SCO connection fails - no more attempts");
  204. return false;
  205. }
  206. if (bluetoothState != State.HEADSET_AVAILABLE) {
  207. Log.e(TAG, "BT SCO connection fails - no headset available");
  208. return false;
  209. }
  210. // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
  211. Log.d(TAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED...");
  212. // The SCO connection establishment can take several seconds, hence we cannot rely on the
  213. // connection to be available when the method returns but instead register to receive the
  214. // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED.
  215. bluetoothState = State.SCO_CONNECTING;
  216. audioManager.startBluetoothSco();
  217. audioManager.setBluetoothScoOn(true);
  218. scoConnectionAttempts++;
  219. startTimer();
  220. Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", "
  221. + "SCO is on: " + isScoOn());
  222. return true;
  223. }
  224. /**
  225. * Stops Bluetooth SCO connection with remote device.
  226. */
  227. public void stopScoAudio() {
  228. ThreadUtils.checkIsOnMainThread();
  229. Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", "
  230. + "SCO is on: " + isScoOn());
  231. if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
  232. return;
  233. }
  234. cancelTimer();
  235. audioManager.stopBluetoothSco();
  236. audioManager.setBluetoothScoOn(false);
  237. bluetoothState = State.SCO_DISCONNECTING;
  238. Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", "
  239. + "SCO is on: " + isScoOn());
  240. }
  241. /**
  242. * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset
  243. * Service via IPC) to update the list of connected devices for the HEADSET
  244. * profile. The internal state will change to HEADSET_UNAVAILABLE or to
  245. * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected
  246. * device if available.
  247. */
  248. @SuppressLint("MissingPermission")
  249. public void updateDevice() {
  250. boolean hasNoBluetoothPermissions = hasNoBluetoothPermission();
  251. if (hasNoBluetoothPermissions ||
  252. bluetoothState == State.UNINITIALIZED ||
  253. bluetoothHeadset == null) {
  254. return;
  255. }
  256. Log.d(TAG, "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(TAG, "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(TAG, "Connected bluetooth headset: "
  270. + "name=" + bluetoothDevice.getName() + ", "
  271. + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
  272. + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
  273. }
  274. Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
  275. }
  276. /**
  277. * Stubs for test mocks.
  278. */
  279. protected AudioManager getAudioManager(Context context) {
  280. return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
  281. }
  282. protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
  283. apprtcContext.registerReceiver(receiver, filter);
  284. }
  285. protected void unregisterReceiver(BroadcastReceiver receiver) {
  286. apprtcContext.unregisterReceiver(receiver);
  287. }
  288. protected boolean getBluetoothProfileProxy(
  289. Context context, BluetoothProfile.ServiceListener listener, int profile) {
  290. return bluetoothAdapter.getProfileProxy(context, listener, profile);
  291. }
  292. private boolean hasNoBluetoothPermission() {
  293. String permission;
  294. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
  295. permission = Manifest.permission.BLUETOOTH_CONNECT;
  296. } else {
  297. permission = Manifest.permission.BLUETOOTH;
  298. }
  299. boolean hasPermission =
  300. ActivityCompat.checkSelfPermission(apprtcContext, permission) == PackageManager.PERMISSION_GRANTED;
  301. if(!hasPermission) {
  302. Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks \"" + permission + "\" permission");
  303. }
  304. return !hasPermission;
  305. }
  306. /**
  307. * Logs the state of the local Bluetooth adapter.
  308. */
  309. @SuppressLint({"HardwareIds", "MissingPermission"})
  310. private void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
  311. Log.d(TAG, "BluetoothAdapter: "
  312. + "enabled=" + localAdapter.isEnabled() + ", "
  313. + "state=" + stateToString(localAdapter.getState()) + ", "
  314. + "name=" + localAdapter.getName());
  315. // Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter.
  316. Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices();
  317. if (!pairedDevices.isEmpty()) {
  318. Log.d(TAG, "paired devices:");
  319. for (BluetoothDevice device : pairedDevices) {
  320. Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
  321. }
  322. }
  323. }
  324. /**
  325. * Ensures that the audio manager updates its list of available audio devices.
  326. */
  327. private void updateAudioDeviceState() {
  328. ThreadUtils.checkIsOnMainThread();
  329. Log.d(TAG, "updateAudioDeviceState");
  330. apprtcAudioManager.updateAudioDeviceState();
  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(TAG, "startTimer");
  338. handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
  339. }
  340. /**
  341. * Cancels any outstanding timer tasks.
  342. */
  343. private void cancelTimer() {
  344. ThreadUtils.checkIsOnMainThread();
  345. Log.d(TAG, "cancelTimer");
  346. handler.removeCallbacks(bluetoothTimeoutRunnable);
  347. }
  348. /**
  349. * Called when start of the BT SCO channel takes too long time. Usually
  350. * happens when the BT device has been turned on during an ongoing call.
  351. */
  352. @SuppressLint("MissingPermission")
  353. private void bluetoothTimeout() {
  354. ThreadUtils.checkIsOnMainThread();
  355. boolean hasNoBluetoothPermissions = hasNoBluetoothPermission();
  356. if (hasNoBluetoothPermissions ||
  357. bluetoothState == State.UNINITIALIZED ||
  358. bluetoothHeadset == null) {
  359. return;
  360. }
  361. Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "
  362. + "attempts: " + scoConnectionAttempts + ", "
  363. + "SCO is on: " + isScoOn());
  364. if (bluetoothState != State.SCO_CONNECTING) {
  365. return;
  366. }
  367. // Bluetooth SCO should be connecting; check the latest result.
  368. boolean scoConnected = false;
  369. List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
  370. if (devices.size() > 0) {
  371. bluetoothDevice = devices.get(0);
  372. if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
  373. Log.d(TAG, "SCO connected with " + bluetoothDevice.getName());
  374. scoConnected = true;
  375. } else {
  376. Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
  377. }
  378. }
  379. if (scoConnected) {
  380. // We thought BT had timed out, but it's actually on; updating state.
  381. bluetoothState = State.SCO_CONNECTED;
  382. scoConnectionAttempts = 0;
  383. } else {
  384. // Give up and "cancel" our request by calling stopBluetoothSco().
  385. Log.w(TAG, "BT failed to connect after timeout");
  386. stopScoAudio();
  387. }
  388. updateAudioDeviceState();
  389. Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
  390. }
  391. /**
  392. * Checks whether audio uses Bluetooth SCO.
  393. */
  394. private boolean isScoOn() {
  395. return audioManager.isBluetoothScoOn();
  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. // 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. * 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(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
  459. // Android only supports one connected Bluetooth Headset at a time.
  460. bluetoothHeadset = (BluetoothHeadset) proxy;
  461. updateAudioDeviceState();
  462. Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
  463. }
  464. @Override
  465. /** Notifies the client when the proxy object has been disconnected from the service. */
  466. public void onServiceDisconnected(int profile) {
  467. if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
  468. return;
  469. }
  470. Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
  471. stopScoAudio();
  472. bluetoothHeadset = null;
  473. bluetoothDevice = null;
  474. bluetoothState = State.HEADSET_UNAVAILABLE;
  475. updateAudioDeviceState();
  476. Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
  477. }
  478. }
  479. // Intent broadcast receiver which handles changes in Bluetooth device availability.
  480. // Detects headset changes and Bluetooth SCO state changes.
  481. private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {
  482. @Override
  483. public void onReceive(Context context, Intent intent) {
  484. if (bluetoothState == State.UNINITIALIZED) {
  485. return;
  486. }
  487. final String action = intent.getAction();
  488. // Change in connection state of the Headset profile. Note that the
  489. // change does not tell us anything about whether we're streaming
  490. // audio to BT over SCO. Typically received when user turns on a BT
  491. // headset while audio is active using another audio device.
  492. if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
  493. final int state =
  494. intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
  495. Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: "
  496. + "a=ACTION_CONNECTION_STATE_CHANGED, "
  497. + "s=" + stateToString(state) + ", "
  498. + "sb=" + isInitialStickyBroadcast() + ", "
  499. + "BT state: " + bluetoothState);
  500. if (state == BluetoothHeadset.STATE_CONNECTED) {
  501. scoConnectionAttempts = 0;
  502. updateAudioDeviceState();
  503. } else if (state == BluetoothHeadset.STATE_CONNECTING) {
  504. // No action needed.
  505. } else if (state == BluetoothHeadset.STATE_DISCONNECTING) {
  506. // No action needed.
  507. } else if (state == BluetoothHeadset.STATE_DISCONNECTED) {
  508. // Bluetooth is probably powered off during the call.
  509. stopScoAudio();
  510. updateAudioDeviceState();
  511. }
  512. // Change in the audio (SCO) connection state of the Headset profile.
  513. // Typically received after call to startScoAudio() has finalized.
  514. } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
  515. final int state = intent.getIntExtra(
  516. BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
  517. Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: "
  518. + "a=ACTION_AUDIO_STATE_CHANGED, "
  519. + "s=" + stateToString(state) + ", "
  520. + "sb=" + isInitialStickyBroadcast() + ", "
  521. + "BT state: " + bluetoothState);
  522. if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
  523. cancelTimer();
  524. if (bluetoothState == State.SCO_CONNECTING) {
  525. Log.d(TAG, "+++ Bluetooth audio SCO is now connected");
  526. bluetoothState = State.SCO_CONNECTED;
  527. scoConnectionAttempts = 0;
  528. updateAudioDeviceState();
  529. } else {
  530. Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
  531. }
  532. } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
  533. Log.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
  534. } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
  535. Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected");
  536. if (isInitialStickyBroadcast()) {
  537. Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
  538. return;
  539. }
  540. updateAudioDeviceState();
  541. }
  542. }
  543. Log.d(TAG, "onReceive done: BT state=" + bluetoothState);
  544. }
  545. }
  546. }