add permission checks to appRTCBluetoothManager

This commit is contained in:
Daniel Gultsch 2022-07-15 09:31:43 +02:00
parent 50ba165746
commit 52ff6f446c
2 changed files with 172 additions and 146 deletions

View file

@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />

View file

@ -9,6 +9,7 @@
*/ */
package eu.siacs.conversations.services; package eu.siacs.conversations.services;
import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
@ -20,25 +21,25 @@ import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Process;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import com.google.common.collect.ImmutableList;
import org.webrtc.ThreadUtils; import org.webrtc.ThreadUtils;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.AppRTCUtils; import eu.siacs.conversations.utils.AppRTCUtils;
/** /** AppRTCProximitySensor manages functions related to Bluetoth devices in the AppRTC demo. */
* AppRTCProximitySensor manages functions related to Bluetoth devices in the
* AppRTC demo.
*/
public class AppRTCBluetoothManager { public class AppRTCBluetoothManager {
// Timeout interval for starting or stopping audio to a Bluetooth SCO device. // Timeout interval for starting or stopping audio to a Bluetooth SCO device.
private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000;
@ -46,28 +47,26 @@ public class AppRTCBluetoothManager {
private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2;
private final Context apprtcContext; private final Context apprtcContext;
private final AppRTCAudioManager apprtcAudioManager; private final AppRTCAudioManager apprtcAudioManager;
@Nullable @Nullable private final AudioManager audioManager;
private final AudioManager audioManager;
private final Handler handler; private final Handler handler;
private final BluetoothProfile.ServiceListener bluetoothServiceListener; private final BluetoothProfile.ServiceListener bluetoothServiceListener;
private final BroadcastReceiver bluetoothHeadsetReceiver; private final BroadcastReceiver bluetoothHeadsetReceiver;
int scoConnectionAttempts; int scoConnectionAttempts;
private State bluetoothState; private State bluetoothState;
@Nullable @Nullable private BluetoothAdapter bluetoothAdapter;
private BluetoothAdapter bluetoothAdapter; @Nullable private BluetoothHeadset bluetoothHeadset;
@Nullable @Nullable private BluetoothDevice bluetoothDevice;
private BluetoothHeadset bluetoothHeadset;
@Nullable
private BluetoothDevice bluetoothDevice;
// Runs when the Bluetooth timeout expires. We use that timeout after calling // Runs when the Bluetooth timeout expires. We use that timeout after calling
// startScoAudio() or stopScoAudio() because we're not guaranteed to get a // startScoAudio() or stopScoAudio() because we're not guaranteed to get a
// callback after those calls. // callback after those calls.
private final Runnable bluetoothTimeoutRunnable = new Runnable() { private final Runnable bluetoothTimeoutRunnable =
new Runnable() {
@Override @Override
public void run() { public void run() {
bluetoothTimeout(); bluetoothTimeout();
} }
}; };
protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) { protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) {
Log.d(Config.LOGTAG, "ctor"); Log.d(Config.LOGTAG, "ctor");
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
@ -80,42 +79,29 @@ public class AppRTCBluetoothManager {
handler = new Handler(Looper.getMainLooper()); handler = new Handler(Looper.getMainLooper());
} }
/** /** Construction. */
* Construction.
*/
static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) { static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) {
Log.d(Config.LOGTAG, "create" + AppRTCUtils.getThreadInfo()); Log.d(Config.LOGTAG, "create" + AppRTCUtils.getThreadInfo());
return new AppRTCBluetoothManager(context, audioManager); return new AppRTCBluetoothManager(context, audioManager);
} }
/** /** Returns the internal state. */
* Returns the internal state.
*/
public State getState() { public State getState() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
return bluetoothState; return bluetoothState;
} }
/** /**
* Activates components required to detect Bluetooth devices and to enable * Activates components required to detect Bluetooth devices and to enable BT SCO (audio is
* BT SCO (audio is routed via BT SCO) for the headset profile. The end * routed via BT SCO) for the headset profile. The end state will be HEADSET_UNAVAILABLE but a
* state will be HEADSET_UNAVAILABLE but a state machine has started which * state machine has started which will start a state change sequence where the final outcome
* will start a state change sequence where the final outcome depends on * depends on if/when the BT headset is enabled. Example of state change sequence when start()
* if/when the BT headset is enabled. * is called while BT device is connected and enabled: UNINITIALIZED --> HEADSET_UNAVAILABLE -->
* Example of state change sequence when start() is called while BT device * HEADSET_AVAILABLE --> SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO.
* is connected and enabled: * Note that the AppRTCAudioManager is also involved in driving this state change.
* UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE -->
* SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO.
* Note that the AppRTCAudioManager is also involved in driving this state
* change.
*/ */
public void start() { public void start() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "start");
if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
Log.w(Config.LOGTAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
return;
}
if (bluetoothState != State.UNINITIALIZED) { if (bluetoothState != State.UNINITIALIZED) {
Log.w(Config.LOGTAG, "Invalid BT state"); Log.w(Config.LOGTAG, "Invalid BT state");
return; return;
@ -130,11 +116,10 @@ public class AppRTCBluetoothManager {
return; return;
} }
// Ensure that the device supports use of BT SCO audio for off call use cases. // Ensure that the device supports use of BT SCO audio for off call use cases.
if (!audioManager.isBluetoothScoAvailableOffCall()) { if (this.audioManager == null || !audioManager.isBluetoothScoAvailableOffCall()) {
Log.e(Config.LOGTAG, "Bluetooth SCO audio is not available off call"); Log.e(Config.LOGTAG, "Bluetooth SCO audio is not available off call");
return; return;
} }
logBluetoothAdapterInfo(bluetoothAdapter);
// Establish a connection to the HEADSET profile (includes both Bluetooth Headset and // Establish a connection to the HEADSET profile (includes both Bluetooth Headset and
// Hands-Free) proxy object and install a listener. // Hands-Free) proxy object and install a listener.
if (!getBluetoothProfileProxy( if (!getBluetoothProfileProxy(
@ -149,16 +134,20 @@ public class AppRTCBluetoothManager {
// Register receiver for change in audio connection state of the Headset profile. // Register receiver for change in audio connection state of the Headset profile.
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter); registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter);
Log.d(Config.LOGTAG, "HEADSET profile state: " if (hasBluetoothConnectPermission()) {
+ stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET))); Log.d(
Config.LOGTAG,
"HEADSET profile state: "
+ stateToString(
bluetoothAdapter.getProfileConnectionState(
BluetoothProfile.HEADSET)));
}
Log.d(Config.LOGTAG, "Bluetooth proxy for headset profile has started"); Log.d(Config.LOGTAG, "Bluetooth proxy for headset profile has started");
bluetoothState = State.HEADSET_UNAVAILABLE; bluetoothState = State.HEADSET_UNAVAILABLE;
Log.d(Config.LOGTAG, "start done: BT state=" + bluetoothState); Log.d(Config.LOGTAG, "start done: BT state=" + bluetoothState);
} }
/** /** Stops and closes all components related to Bluetooth audio. */
* Stops and closes all components related to Bluetooth audio.
*/
public void stop() { public void stop() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "stop: BT state=" + bluetoothState); Log.d(Config.LOGTAG, "stop: BT state=" + bluetoothState);
@ -184,23 +173,29 @@ public class AppRTCBluetoothManager {
} }
/** /**
* Starts Bluetooth SCO connection with remote device. * Starts Bluetooth SCO connection with remote device. Note that the phone application always
* Note that the phone application always has the priority on the usage of the SCO connection * has the priority on the usage of the SCO connection for telephony. If this method is called
* for telephony. If this method is called while the phone is in call it will be ignored. * while the phone is in call it will be ignored. Similarly, if a call is received or sent while
* Similarly, if a call is received or sent while an application is using the SCO connection, * an application is using the SCO connection, the connection will be lost for the application
* the connection will be lost for the application and NOT returned automatically when the call * and NOT returned automatically when the call ends. Also note that: up to and including API
* ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a * version JELLY_BEAN_MR1, this method initiates a virtual voice call to the Bluetooth headset.
* virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO * After API version JELLY_BEAN_MR2 only a raw SCO audio connection is established.
* audio connection is established.
* TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and * TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and
* higher. It might be required to initiates a virtual voice call since many devices do not * higher. It might be required to initiates a virtual voice call since many devices do not
* accept SCO audio without a "call". * accept SCO audio without a "call".
*/ */
public boolean startScoAudio() { public boolean startScoAudio() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "startSco: BT state=" + bluetoothState + ", " Log.d(
+ "attempts: " + scoConnectionAttempts + ", " Config.LOGTAG,
+ "SCO is on: " + isScoOn()); "startSco: BT state="
+ bluetoothState
+ ", "
+ "attempts: "
+ scoConnectionAttempts
+ ", "
+ "SCO is on: "
+ isScoOn());
if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) { if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
Log.e(Config.LOGTAG, "BT SCO connection fails - no more attempts"); Log.e(Config.LOGTAG, "BT SCO connection fails - no more attempts");
return false; return false;
@ -213,24 +208,29 @@ public class AppRTCBluetoothManager {
Log.d(Config.LOGTAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED..."); Log.d(Config.LOGTAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED...");
// The SCO connection establishment can take several seconds, hence we cannot rely on the // The SCO connection establishment can take several seconds, hence we cannot rely on the
// connection to be available when the method returns but instead register to receive the // connection to be available when the method returns but instead register to receive the
// intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED. // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be
// SCO_AUDIO_STATE_CONNECTED.
bluetoothState = State.SCO_CONNECTING; bluetoothState = State.SCO_CONNECTING;
audioManager.startBluetoothSco(); audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true); audioManager.setBluetoothScoOn(true);
scoConnectionAttempts++; scoConnectionAttempts++;
startTimer(); startTimer();
Log.d(Config.LOGTAG, "startScoAudio done: BT state=" + bluetoothState + ", " Log.d(
+ "SCO is on: " + isScoOn()); Config.LOGTAG,
"startScoAudio done: BT state="
+ bluetoothState
+ ", "
+ "SCO is on: "
+ isScoOn());
return true; return true;
} }
/** /** Stops Bluetooth SCO connection with remote device. */
* Stops Bluetooth SCO connection with remote device.
*/
public void stopScoAudio() { public void stopScoAudio() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "stopScoAudio: BT state=" + bluetoothState + ", " Log.d(
+ "SCO is on: " + isScoOn()); Config.LOGTAG,
"stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn());
if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) { if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
return; return;
} }
@ -238,17 +238,18 @@ public class AppRTCBluetoothManager {
audioManager.stopBluetoothSco(); audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false); audioManager.setBluetoothScoOn(false);
bluetoothState = State.SCO_DISCONNECTING; bluetoothState = State.SCO_DISCONNECTING;
Log.d(Config.LOGTAG, "stopScoAudio done: BT state=" + bluetoothState + ", " Log.d(
+ "SCO is on: " + isScoOn()); Config.LOGTAG,
"stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn());
} }
/** /**
* Use the BluetoothHeadset proxy object (controls the Bluetooth Headset * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset Service via IPC) to
* Service via IPC) to update the list of connected devices for the HEADSET * update the list of connected devices for the HEADSET profile. The internal state will change
* profile. The internal state will change to HEADSET_UNAVAILABLE or to * to HEADSET_UNAVAILABLE or to HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the
* HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected * connected device if available.
* device if available.
*/ */
@SuppressLint("MissingPermission")
public void updateDevice() { public void updateDevice() {
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
return; return;
@ -257,7 +258,12 @@ public class AppRTCBluetoothManager {
// Get connected devices for the headset profile. Returns the set of // Get connected devices for the headset profile. Returns the set of
// devices which are in state STATE_CONNECTED. The BluetoothDevice class // devices which are in state STATE_CONNECTED. The BluetoothDevice class
// is just a thin wrapper for a Bluetooth hardware address. // is just a thin wrapper for a Bluetooth hardware address.
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices(); final List<BluetoothDevice> devices;
if (hasBluetoothConnectPermission()) {
devices = bluetoothHeadset.getConnectedDevices();
} else {
devices = ImmutableList.of();
}
if (devices.isEmpty()) { if (devices.isEmpty()) {
bluetoothDevice = null; bluetoothDevice = null;
bluetoothState = State.HEADSET_UNAVAILABLE; bluetoothState = State.HEADSET_UNAVAILABLE;
@ -266,17 +272,21 @@ public class AppRTCBluetoothManager {
// Always use first device in list. Android only supports one device. // Always use first device in list. Android only supports one device.
bluetoothDevice = devices.get(0); bluetoothDevice = devices.get(0);
bluetoothState = State.HEADSET_AVAILABLE; bluetoothState = State.HEADSET_AVAILABLE;
Log.d(Config.LOGTAG, "Connected bluetooth headset: " Log.d(
+ "name=" + bluetoothDevice.getName() + ", " Config.LOGTAG,
+ "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) "Connected bluetooth headset: "
+ ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice)); + "name="
+ bluetoothDevice.getName()
+ ", "
+ "state="
+ stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
+ ", SCO audio="
+ bluetoothHeadset.isAudioConnected(bluetoothDevice));
} }
Log.d(Config.LOGTAG, "updateDevice done: BT state=" + bluetoothState); Log.d(Config.LOGTAG, "updateDevice done: BT state=" + bluetoothState);
} }
/** /** Stubs for test mocks. */
* Stubs for test mocks.
*/
@Nullable @Nullable
protected AudioManager getAudioManager(Context context) { protected AudioManager getAudioManager(Context context) {
return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@ -295,52 +305,31 @@ public class AppRTCBluetoothManager {
return bluetoothAdapter.getProfileProxy(context, listener, profile); return bluetoothAdapter.getProfileProxy(context, listener, profile);
} }
protected boolean hasPermission(Context context, String permission) { protected boolean hasBluetoothConnectPermission() {
return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid()) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return ActivityCompat.checkSelfPermission(
apprtcContext, Manifest.permission.BLUETOOTH_CONNECT)
== PackageManager.PERMISSION_GRANTED; == PackageManager.PERMISSION_GRANTED;
} } else {
return true;
/**
* Logs the state of the local Bluetooth adapter.
*/
@SuppressLint("HardwareIds")
protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
Log.d(Config.LOGTAG, "BluetoothAdapter: "
+ "enabled=" + localAdapter.isEnabled() + ", "
+ "state=" + stateToString(localAdapter.getState()) + ", "
+ "name=" + localAdapter.getName() + ", "
+ "address=" + localAdapter.getAddress());
// Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter.
Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices();
if (!pairedDevices.isEmpty()) {
Log.d(Config.LOGTAG, "paired devices:");
for (BluetoothDevice device : pairedDevices) {
Log.d(Config.LOGTAG, " name=" + device.getName() + ", address=" + device.getAddress());
}
} }
} }
/** /** Ensures that the audio manager updates its list of available audio devices. */
* Ensures that the audio manager updates its list of available audio devices.
*/
private void updateAudioDeviceState() { private void updateAudioDeviceState() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "updateAudioDeviceState"); Log.d(Config.LOGTAG, "updateAudioDeviceState");
apprtcAudioManager.updateAudioDeviceState(); apprtcAudioManager.updateAudioDeviceState();
} }
/** /** Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. */
* Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
*/
private void startTimer() { private void startTimer() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "startTimer"); Log.d(Config.LOGTAG, "startTimer");
handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS); handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
} }
/** /** Cancels any outstanding timer tasks. */
* Cancels any outstanding timer tasks.
*/
private void cancelTimer() { private void cancelTimer() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(Config.LOGTAG, "cancelTimer"); Log.d(Config.LOGTAG, "cancelTimer");
@ -348,23 +337,36 @@ public class AppRTCBluetoothManager {
} }
/** /**
* Called when start of the BT SCO channel takes too long time. Usually * Called when start of the BT SCO channel takes too long time. Usually happens when the BT
* happens when the BT device has been turned on during an ongoing call. * device has been turned on during an ongoing call.
*/ */
@SuppressLint("MissingPermission")
private void bluetoothTimeout() { private void bluetoothTimeout() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
return; return;
} }
Log.d(Config.LOGTAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " Log.d(
+ "attempts: " + scoConnectionAttempts + ", " Config.LOGTAG,
+ "SCO is on: " + isScoOn()); "bluetoothTimeout: BT state="
+ bluetoothState
+ ", "
+ "attempts: "
+ scoConnectionAttempts
+ ", "
+ "SCO is on: "
+ isScoOn());
if (bluetoothState != State.SCO_CONNECTING) { if (bluetoothState != State.SCO_CONNECTING) {
return; return;
} }
// Bluetooth SCO should be connecting; check the latest result. // Bluetooth SCO should be connecting; check the latest result.
boolean scoConnected = false; boolean scoConnected = false;
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices(); final List<BluetoothDevice> devices;
if (hasBluetoothConnectPermission()) {
devices = bluetoothHeadset.getConnectedDevices();
} else {
devices = Collections.emptyList();
}
if (devices.size() > 0) { if (devices.size() > 0) {
bluetoothDevice = devices.get(0); bluetoothDevice = devices.get(0);
if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) { if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
@ -387,16 +389,12 @@ public class AppRTCBluetoothManager {
Log.d(Config.LOGTAG, "bluetoothTimeout done: BT state=" + bluetoothState); Log.d(Config.LOGTAG, "bluetoothTimeout done: BT state=" + bluetoothState);
} }
/** /** Checks whether audio uses Bluetooth SCO. */
* Checks whether audio uses Bluetooth SCO.
*/
private boolean isScoOn() { private boolean isScoOn() {
return audioManager.isBluetoothScoOn(); return audioManager.isBluetoothScoOn();
} }
/** /** Converts BluetoothAdapter states into local string representations. */
* Converts BluetoothAdapter states into local string representations.
*/
private String stateToString(int state) { private String stateToString(int state) {
switch (state) { switch (state) {
case BluetoothAdapter.STATE_DISCONNECTED: case BluetoothAdapter.STATE_DISCONNECTED:
@ -412,11 +410,13 @@ public class AppRTCBluetoothManager {
case BluetoothAdapter.STATE_ON: case BluetoothAdapter.STATE_ON:
return "ON"; return "ON";
case BluetoothAdapter.STATE_TURNING_OFF: case BluetoothAdapter.STATE_TURNING_OFF:
// Indicates the local Bluetooth adapter is turning off. Local clients should immediately // Indicates the local Bluetooth adapter is turning off. Local clients should
// immediately
// attempt graceful disconnection of any remote links. // attempt graceful disconnection of any remote links.
return "TURNING_OFF"; return "TURNING_OFF";
case BluetoothAdapter.STATE_TURNING_ON: case BluetoothAdapter.STATE_TURNING_ON:
// Indicates the local Bluetooth adapter is turning on. However local clients should wait // Indicates the local Bluetooth adapter is turning on. However local clients should
// wait
// for STATE_ON before attempting to use the adapter. // for STATE_ON before attempting to use the adapter.
return "TURNING_ON"; return "TURNING_ON";
default: default:
@ -457,7 +457,9 @@ public class AppRTCBluetoothManager {
if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
return; return;
} }
Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); Log.d(
Config.LOGTAG,
"BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
// Android only supports one connected Bluetooth Headset at a time. // Android only supports one connected Bluetooth Headset at a time.
bluetoothHeadset = (BluetoothHeadset) proxy; bluetoothHeadset = (BluetoothHeadset) proxy;
updateAudioDeviceState(); updateAudioDeviceState();
@ -470,7 +472,9 @@ public class AppRTCBluetoothManager {
if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
return; return;
} }
Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); Log.d(
Config.LOGTAG,
"BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
stopScoAudio(); stopScoAudio();
bluetoothHeadset = null; bluetoothHeadset = null;
bluetoothDevice = null; bluetoothDevice = null;
@ -495,12 +499,20 @@ public class AppRTCBluetoothManager {
// headset while audio is active using another audio device. // headset while audio is active using another audio device.
if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
final int state = final int state =
intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); intent.getIntExtra(
Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
Log.d(
Config.LOGTAG,
"BluetoothHeadsetBroadcastReceiver.onReceive: "
+ "a=ACTION_CONNECTION_STATE_CHANGED, " + "a=ACTION_CONNECTION_STATE_CHANGED, "
+ "s=" + stateToString(state) + ", " + "s="
+ "sb=" + isInitialStickyBroadcast() + ", " + stateToString(state)
+ "BT state: " + bluetoothState); + ", "
+ "sb="
+ isInitialStickyBroadcast()
+ ", "
+ "BT state: "
+ bluetoothState);
if (state == BluetoothHeadset.STATE_CONNECTED) { if (state == BluetoothHeadset.STATE_CONNECTED) {
scoConnectionAttempts = 0; scoConnectionAttempts = 0;
updateAudioDeviceState(); updateAudioDeviceState();
@ -516,13 +528,22 @@ public class AppRTCBluetoothManager {
// Change in the audio (SCO) connection state of the Headset profile. // Change in the audio (SCO) connection state of the Headset profile.
// Typically received after call to startScoAudio() has finalized. // Typically received after call to startScoAudio() has finalized.
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
final int state = intent.getIntExtra( final int state =
BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); intent.getIntExtra(
Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " BluetoothHeadset.EXTRA_STATE,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
Log.d(
Config.LOGTAG,
"BluetoothHeadsetBroadcastReceiver.onReceive: "
+ "a=ACTION_AUDIO_STATE_CHANGED, " + "a=ACTION_AUDIO_STATE_CHANGED, "
+ "s=" + stateToString(state) + ", " + "s="
+ "sb=" + isInitialStickyBroadcast() + ", " + stateToString(state)
+ "BT state: " + bluetoothState); + ", "
+ "sb="
+ isInitialStickyBroadcast()
+ ", "
+ "BT state: "
+ bluetoothState);
if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
cancelTimer(); cancelTimer();
if (bluetoothState == State.SCO_CONNECTING) { if (bluetoothState == State.SCO_CONNECTING) {
@ -531,14 +552,18 @@ public class AppRTCBluetoothManager {
scoConnectionAttempts = 0; scoConnectionAttempts = 0;
updateAudioDeviceState(); updateAudioDeviceState();
} else { } else {
Log.w(Config.LOGTAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); Log.w(
Config.LOGTAG,
"Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
} }
} else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now connecting..."); Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now connecting...");
} else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now disconnected"); Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now disconnected");
if (isInitialStickyBroadcast()) { if (isInitialStickyBroadcast()) {
Log.d(Config.LOGTAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); Log.d(
Config.LOGTAG,
"Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
return; return;
} }
updateAudioDeviceState(); updateAudioDeviceState();