add permission checks to appRTCBluetoothManager
This commit is contained in:
parent
50ba165746
commit
52ff6f446c
|
@ -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" />
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue