Adds basic Bluetooth support to AppRTCMobile
BUG=webrtc:6649 - Supports Bluetooth Headset profile. - Detects new BT headset: + enabled at start, and + powered on during active call. - Enables/disables BT SCO channel when BT device is selected. - Removes proximity sensor usage to avoid conflicts (will be added again later). - Adds new (unused) APIs to explicitly select audio device. - Starts routing audio to BT headset when enabled, i.e, BT is default. Review-Url: https://codereview.webrtc.org/2501983002 Cr-Commit-Position: refs/heads/master@{#15610}
This commit is contained in:
@ -704,6 +704,7 @@ if (rtc_include_tests) {
|
|||||||
"androidjunit/src/org/webrtc/CameraEnumerationTest.java",
|
"androidjunit/src/org/webrtc/CameraEnumerationTest.java",
|
||||||
"examples/androidjunit/src/org/appspot/apprtc/DirectRTCClientTest.java",
|
"examples/androidjunit/src/org/appspot/apprtc/DirectRTCClientTest.java",
|
||||||
"examples/androidjunit/src/org/appspot/apprtc/TCPChannelClientTest.java",
|
"examples/androidjunit/src/org/appspot/apprtc/TCPChannelClientTest.java",
|
||||||
|
"examples/androidjunit/src/org/appspot/apprtc/BluetoothManagerTest.java",
|
||||||
]
|
]
|
||||||
|
|
||||||
deps = [
|
deps = [
|
||||||
|
|||||||
@ -65,6 +65,7 @@ if (is_android) {
|
|||||||
android_library("AppRTCMobile_javalib") {
|
android_library("AppRTCMobile_javalib") {
|
||||||
java_files = [
|
java_files = [
|
||||||
"androidapp/src/org/appspot/apprtc/AppRTCAudioManager.java",
|
"androidapp/src/org/appspot/apprtc/AppRTCAudioManager.java",
|
||||||
|
"androidapp/src/org/appspot/apprtc/AppRTCBluetoothManager.java",
|
||||||
"androidapp/src/org/appspot/apprtc/AppRTCClient.java",
|
"androidapp/src/org/appspot/apprtc/AppRTCClient.java",
|
||||||
"androidapp/src/org/appspot/apprtc/AppRTCProximitySensor.java",
|
"androidapp/src/org/appspot/apprtc/AppRTCProximitySensor.java",
|
||||||
"androidapp/src/org/appspot/apprtc/CallActivity.java",
|
"androidapp/src/org/appspot/apprtc/CallActivity.java",
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|||||||
@ -22,8 +22,11 @@ import android.media.AudioManager;
|
|||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.webrtc.ThreadUtils;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,22 +42,48 @@ public class AppRTCAudioManager {
|
|||||||
* AudioDevice is the names of possible audio devices that we currently
|
* AudioDevice is the names of possible audio devices that we currently
|
||||||
* support.
|
* support.
|
||||||
*/
|
*/
|
||||||
// TODO(henrika): add support for BLUETOOTH as well.
|
public enum AudioDevice { SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE }
|
||||||
public enum AudioDevice {
|
|
||||||
SPEAKER_PHONE,
|
/** AudioManager state. */
|
||||||
WIRED_HEADSET,
|
public enum AudioManagerState {
|
||||||
EARPIECE,
|
UNINITIALIZED,
|
||||||
|
PREINITIALIZED,
|
||||||
|
RUNNING,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Selected audio device change event. */
|
||||||
|
public static interface AudioManagerEvents {
|
||||||
|
// Callback fired once audio device is changed or list of available audio devices changed.
|
||||||
|
void onAudioDeviceChanged(
|
||||||
|
AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Context apprtcContext;
|
private final Context apprtcContext;
|
||||||
private final Runnable onStateChangeListener;
|
|
||||||
private boolean initialized = false;
|
|
||||||
private AudioManager audioManager;
|
private AudioManager audioManager;
|
||||||
|
|
||||||
|
private AudioManagerEvents audioManagerEvents;
|
||||||
|
private AudioManagerState amState;
|
||||||
private int savedAudioMode = AudioManager.MODE_INVALID;
|
private int savedAudioMode = AudioManager.MODE_INVALID;
|
||||||
private boolean savedIsSpeakerPhoneOn = false;
|
private boolean savedIsSpeakerPhoneOn = false;
|
||||||
private boolean savedIsMicrophoneMute = false;
|
private boolean savedIsMicrophoneMute = false;
|
||||||
|
private boolean hasWiredHeadset = false;
|
||||||
|
|
||||||
private final AudioDevice defaultAudioDevice;
|
// Default audio device; speaker phone for video calls or earpiece for audio
|
||||||
|
// only calls.
|
||||||
|
private AudioDevice defaultAudioDevice;
|
||||||
|
|
||||||
|
// Contains the currently selected audio device.
|
||||||
|
// This device is changed automatically using a certain scheme where e.g.
|
||||||
|
// a wired headset "wins" over speaker phone. It is also possible for a
|
||||||
|
// user to explicitly select a device (and overrid any predefined scheme).
|
||||||
|
// See |userSelectedAudioDevice| for details.
|
||||||
|
private AudioDevice selectedAudioDevice;
|
||||||
|
|
||||||
|
// Contains the user-selected audio device which overrides the predefined
|
||||||
|
// selection scheme.
|
||||||
|
// TODO(henrika): always set to AudioDevice.NONE today. Add support for
|
||||||
|
// explicit selection based on choice by userSelectedAudioDevice.
|
||||||
|
private AudioDevice userSelectedAudioDevice;
|
||||||
|
|
||||||
// Contains speakerphone setting: auto, true or false
|
// Contains speakerphone setting: auto, true or false
|
||||||
private final String useSpeakerphone;
|
private final String useSpeakerphone;
|
||||||
@ -65,12 +94,12 @@ public class AppRTCAudioManager {
|
|||||||
// available, far from ear <=> use speaker phone).
|
// available, far from ear <=> use speaker phone).
|
||||||
private AppRTCProximitySensor proximitySensor = null;
|
private AppRTCProximitySensor proximitySensor = null;
|
||||||
|
|
||||||
// Contains the currently selected audio device.
|
// Handles all tasks related to Bluetooth headset devices.
|
||||||
private AudioDevice selectedAudioDevice;
|
private final AppRTCBluetoothManager bluetoothManager;
|
||||||
|
|
||||||
// Contains a list of available audio devices. A Set collection is used to
|
// Contains a list of available audio devices. A Set collection is used to
|
||||||
// avoid duplicate elements.
|
// avoid duplicate elements.
|
||||||
private final Set<AudioDevice> audioDevices = new HashSet<AudioDevice>();
|
private Set<AudioDevice> audioDevices = new HashSet<AudioDevice>();
|
||||||
|
|
||||||
// Broadcast receiver for wired headset intent broadcasts.
|
// Broadcast receiver for wired headset intent broadcasts.
|
||||||
private BroadcastReceiver wiredHeadsetReceiver;
|
private BroadcastReceiver wiredHeadsetReceiver;
|
||||||
@ -78,8 +107,10 @@ public class AppRTCAudioManager {
|
|||||||
// Callback method for changes in audio focus.
|
// Callback method for changes in audio focus.
|
||||||
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
||||||
|
|
||||||
// This method is called when the proximity sensor reports a state change,
|
/**
|
||||||
// e.g. from "NEAR to FAR" or from "FAR to NEAR".
|
* This method is called when the proximity sensor reports a state change,
|
||||||
|
* e.g. from "NEAR to FAR" or from "FAR to NEAR".
|
||||||
|
*/
|
||||||
private void onProximitySensorChangedState() {
|
private void onProximitySensorChangedState() {
|
||||||
if (!useSpeakerphone.equals(SPEAKERPHONE_AUTO)) {
|
if (!useSpeakerphone.equals(SPEAKERPHONE_AUTO)) {
|
||||||
return;
|
return;
|
||||||
@ -92,29 +123,55 @@ public class AppRTCAudioManager {
|
|||||||
if (proximitySensor.sensorReportsNearState()) {
|
if (proximitySensor.sensorReportsNearState()) {
|
||||||
// Sensor reports that a "handset is being held up to a person's ear",
|
// Sensor reports that a "handset is being held up to a person's ear",
|
||||||
// or "something is covering the light sensor".
|
// or "something is covering the light sensor".
|
||||||
setAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
|
setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.EARPIECE);
|
||||||
} else {
|
} else {
|
||||||
// Sensor reports that a "handset is removed from a person's ear", or
|
// Sensor reports that a "handset is removed from a person's ear", or
|
||||||
// "the light sensor is no longer covered".
|
// "the light sensor is no longer covered".
|
||||||
setAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
|
setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Construction */
|
/* Receiver which handles changes in wired headset availability. */
|
||||||
static AppRTCAudioManager create(Context context, Runnable deviceStateChangeListener) {
|
private class WiredHeadsetReceiver extends BroadcastReceiver {
|
||||||
return new AppRTCAudioManager(context, deviceStateChangeListener);
|
private static final int STATE_UNPLUGGED = 0;
|
||||||
|
private static final int STATE_PLUGGED = 1;
|
||||||
|
private static final int HAS_NO_MIC = 0;
|
||||||
|
private static final int HAS_MIC = 1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
int state = intent.getIntExtra("state", STATE_UNPLUGGED);
|
||||||
|
int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
|
||||||
|
String name = intent.getStringExtra("name");
|
||||||
|
Log.d(TAG, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": "
|
||||||
|
+ "a=" + intent.getAction() + ", s="
|
||||||
|
+ (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m="
|
||||||
|
+ (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb="
|
||||||
|
+ isInitialStickyBroadcast());
|
||||||
|
hasWiredHeadset = (state == STATE_PLUGGED);
|
||||||
|
updateAudioDeviceState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Construction. */
|
||||||
|
static AppRTCAudioManager create(Context context) {
|
||||||
|
return new AppRTCAudioManager(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppRTCAudioManager(Context context, Runnable deviceStateChangeListener) {
|
private AppRTCAudioManager(Context context) {
|
||||||
|
Log.d(TAG, "ctor");
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
apprtcContext = context;
|
apprtcContext = context;
|
||||||
onStateChangeListener = deviceStateChangeListener;
|
|
||||||
audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
|
audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
|
||||||
|
bluetoothManager = AppRTCBluetoothManager.create(context, this);
|
||||||
|
wiredHeadsetReceiver = new WiredHeadsetReceiver();
|
||||||
|
amState = AudioManagerState.UNINITIALIZED;
|
||||||
|
|
||||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
useSpeakerphone = sharedPreferences.getString(context.getString(R.string.pref_speakerphone_key),
|
useSpeakerphone = sharedPreferences.getString(context.getString(R.string.pref_speakerphone_key),
|
||||||
context.getString(R.string.pref_speakerphone_default));
|
context.getString(R.string.pref_speakerphone_default));
|
||||||
|
Log.d(TAG, "useSpeakerphone: " + useSpeakerphone);
|
||||||
if (useSpeakerphone.equals(SPEAKERPHONE_FALSE)) {
|
if (useSpeakerphone.equals(SPEAKERPHONE_FALSE)) {
|
||||||
defaultAudioDevice = AudioDevice.EARPIECE;
|
defaultAudioDevice = AudioDevice.EARPIECE;
|
||||||
} else {
|
} else {
|
||||||
@ -132,19 +189,29 @@ public class AppRTCAudioManager {
|
|||||||
onProximitySensorChangedState();
|
onProximitySensorChangedState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Log.d(TAG, "defaultAudioDevice: " + defaultAudioDevice);
|
||||||
AppRTCUtils.logDeviceInfo(TAG);
|
AppRTCUtils.logDeviceInfo(TAG);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init() {
|
public void start(AudioManagerEvents audioManagerEvents) {
|
||||||
Log.d(TAG, "init");
|
Log.d(TAG, "start");
|
||||||
if (initialized) {
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
if (amState == AudioManagerState.RUNNING) {
|
||||||
|
Log.e(TAG, "AudioManager is already active");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED.
|
||||||
|
|
||||||
// Store current audio state so we can restore it when close() is called.
|
Log.d(TAG, "AudioManager starts...");
|
||||||
|
this.audioManagerEvents = audioManagerEvents;
|
||||||
|
amState = AudioManagerState.RUNNING;
|
||||||
|
|
||||||
|
// Store current audio state so we can restore it when stop() is called.
|
||||||
savedAudioMode = audioManager.getMode();
|
savedAudioMode = audioManager.getMode();
|
||||||
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
|
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
|
||||||
savedIsMicrophoneMute = audioManager.isMicrophoneMute();
|
savedIsMicrophoneMute = audioManager.isMicrophoneMute();
|
||||||
|
hasWiredHeadset = hasWiredHeadset();
|
||||||
|
|
||||||
// Create an AudioManager.OnAudioFocusChangeListener instance.
|
// Create an AudioManager.OnAudioFocusChangeListener instance.
|
||||||
audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
|
audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
|
||||||
@ -199,31 +266,43 @@ public class AppRTCAudioManager {
|
|||||||
// Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
|
// Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
|
||||||
// required to be in this mode when playout and/or recording starts for
|
// required to be in this mode when playout and/or recording starts for
|
||||||
// best possible VoIP performance.
|
// best possible VoIP performance.
|
||||||
// TODO(henrika): we migh want to start with RINGTONE mode here instead.
|
|
||||||
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
||||||
|
|
||||||
// Always disable microphone mute during a WebRTC call.
|
// Always disable microphone mute during a WebRTC call.
|
||||||
setMicrophoneMute(false);
|
setMicrophoneMute(false);
|
||||||
|
|
||||||
|
// Set initial device states.
|
||||||
|
userSelectedAudioDevice = AudioDevice.NONE;
|
||||||
|
selectedAudioDevice = AudioDevice.NONE;
|
||||||
|
audioDevices.clear();
|
||||||
|
|
||||||
|
// Initialize and start Bluetooth if a BT device is available or initiate
|
||||||
|
// detection of new (enabled) BT devices.
|
||||||
|
bluetoothManager.start();
|
||||||
|
|
||||||
// Do initial selection of audio device. This setting can later be changed
|
// Do initial selection of audio device. This setting can later be changed
|
||||||
// either by adding/removing a wired headset or by covering/uncovering the
|
// either by adding/removing a BT or wired headset or by covering/uncovering
|
||||||
// proximity sensor.
|
// the proximity sensor.
|
||||||
updateAudioDeviceState(hasWiredHeadset());
|
updateAudioDeviceState();
|
||||||
|
|
||||||
// Register receiver for broadcast intents related to adding/removing a
|
// Register receiver for broadcast intents related to adding/removing a
|
||||||
// wired headset (Intent.ACTION_HEADSET_PLUG).
|
// wired headset.
|
||||||
registerForWiredHeadsetIntentBroadcast();
|
registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
|
||||||
|
Log.d(TAG, "AudioManager started");
|
||||||
initialized = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void stop() {
|
||||||
Log.d(TAG, "close");
|
Log.d(TAG, "stop");
|
||||||
if (!initialized) {
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
if (amState != AudioManagerState.RUNNING) {
|
||||||
|
Log.e(TAG, "Trying to stop AudioManager in incorrect state: " + amState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
amState = AudioManagerState.UNINITIALIZED;
|
||||||
|
|
||||||
unregisterForWiredHeadsetIntentBroadcast();
|
unregisterReceiver(wiredHeadsetReceiver);
|
||||||
|
|
||||||
|
bluetoothManager.stop();
|
||||||
|
|
||||||
// Restore previously stored audio states.
|
// Restore previously stored audio states.
|
||||||
setSpeakerphoneOn(savedIsSpeakerPhoneOn);
|
setSpeakerphoneOn(savedIsSpeakerPhoneOn);
|
||||||
@ -240,94 +319,90 @@ public class AppRTCAudioManager {
|
|||||||
proximitySensor = null;
|
proximitySensor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
initialized = false;
|
audioManagerEvents = null;
|
||||||
|
Log.d(TAG, "AudioManager stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Changes selection of the currently active audio device. */
|
/** Changes selection of the currently active audio device. */
|
||||||
public void setAudioDevice(AudioDevice device) {
|
private void setAudioDeviceInternal(AudioDevice device) {
|
||||||
Log.d(TAG, "setAudioDevice(device=" + device + ")");
|
Log.d(TAG, "setAudioDeviceInternal(device=" + device + ")");
|
||||||
AppRTCUtils.assertIsTrue(audioDevices.contains(device));
|
AppRTCUtils.assertIsTrue(audioDevices.contains(device));
|
||||||
|
|
||||||
switch (device) {
|
switch (device) {
|
||||||
case SPEAKER_PHONE:
|
case SPEAKER_PHONE:
|
||||||
setSpeakerphoneOn(true);
|
setSpeakerphoneOn(true);
|
||||||
selectedAudioDevice = AudioDevice.SPEAKER_PHONE;
|
|
||||||
break;
|
break;
|
||||||
case EARPIECE:
|
case EARPIECE:
|
||||||
setSpeakerphoneOn(false);
|
setSpeakerphoneOn(false);
|
||||||
selectedAudioDevice = AudioDevice.EARPIECE;
|
|
||||||
break;
|
break;
|
||||||
case WIRED_HEADSET:
|
case WIRED_HEADSET:
|
||||||
setSpeakerphoneOn(false);
|
setSpeakerphoneOn(false);
|
||||||
selectedAudioDevice = AudioDevice.WIRED_HEADSET;
|
break;
|
||||||
|
case BLUETOOTH:
|
||||||
|
setSpeakerphoneOn(false);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Log.e(TAG, "Invalid audio device selection");
|
Log.e(TAG, "Invalid audio device selection");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
onAudioManagerChangedState();
|
selectedAudioDevice = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes default audio device.
|
||||||
|
* TODO(henrika): add usage of this method in the AppRTCMobile client.
|
||||||
|
*/
|
||||||
|
public void setDefaultAudioDevice(AudioDevice defaultDevice) {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
switch (defaultDevice) {
|
||||||
|
case SPEAKER_PHONE:
|
||||||
|
defaultAudioDevice = defaultDevice;
|
||||||
|
break;
|
||||||
|
case EARPIECE:
|
||||||
|
if (hasEarpiece()) {
|
||||||
|
defaultAudioDevice = defaultDevice;
|
||||||
|
} else {
|
||||||
|
defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.e(TAG, "Invalid default audio device selection");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")");
|
||||||
|
updateAudioDeviceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Changes selection of the currently active audio device. */
|
||||||
|
public void selectAudioDevice(AudioDevice device) {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
if (!audioDevices.contains(device)) {
|
||||||
|
Log.e(TAG, "Can not select " + device + " from available " + audioDevices);
|
||||||
|
}
|
||||||
|
userSelectedAudioDevice = device;
|
||||||
|
updateAudioDeviceState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns current set of available/selectable audio devices. */
|
/** Returns current set of available/selectable audio devices. */
|
||||||
public Set<AudioDevice> getAudioDevices() {
|
public Set<AudioDevice> getAudioDevices() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices));
|
return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the currently selected audio device. */
|
/** Returns the currently selected audio device. */
|
||||||
public AudioDevice getSelectedAudioDevice() {
|
public AudioDevice getSelectedAudioDevice() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
return selectedAudioDevice;
|
return selectedAudioDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Helper method for receiver registration. */
|
||||||
* Registers receiver for the broadcasted intent when a wired headset is
|
private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
|
||||||
* plugged in or unplugged. The received intent will have an extra
|
apprtcContext.registerReceiver(receiver, filter);
|
||||||
* 'state' value where 0 means unplugged, and 1 means plugged.
|
|
||||||
*/
|
|
||||||
private void registerForWiredHeadsetIntentBroadcast() {
|
|
||||||
IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
|
|
||||||
|
|
||||||
/** Receiver which handles changes in wired headset availability. */
|
|
||||||
wiredHeadsetReceiver = new BroadcastReceiver() {
|
|
||||||
private static final int STATE_UNPLUGGED = 0;
|
|
||||||
private static final int STATE_PLUGGED = 1;
|
|
||||||
private static final int HAS_NO_MIC = 0;
|
|
||||||
private static final int HAS_MIC = 1;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
int state = intent.getIntExtra("state", STATE_UNPLUGGED);
|
|
||||||
int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
|
|
||||||
String name = intent.getStringExtra("name");
|
|
||||||
Log.d(TAG, "BroadcastReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": "
|
|
||||||
+ "a=" + intent.getAction() + ", s="
|
|
||||||
+ (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m="
|
|
||||||
+ (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb="
|
|
||||||
+ isInitialStickyBroadcast());
|
|
||||||
|
|
||||||
boolean hasWiredHeadset = (state == STATE_PLUGGED);
|
|
||||||
switch (state) {
|
|
||||||
case STATE_UNPLUGGED:
|
|
||||||
updateAudioDeviceState(hasWiredHeadset);
|
|
||||||
break;
|
|
||||||
case STATE_PLUGGED:
|
|
||||||
if (selectedAudioDevice != AudioDevice.WIRED_HEADSET) {
|
|
||||||
updateAudioDeviceState(hasWiredHeadset);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Log.e(TAG, "Invalid state");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
apprtcContext.registerReceiver(wiredHeadsetReceiver, filter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */
|
/** Helper method for unregistration of an existing receiver. */
|
||||||
private void unregisterForWiredHeadsetIntentBroadcast() {
|
private void unregisterReceiver(BroadcastReceiver receiver) {
|
||||||
apprtcContext.unregisterReceiver(wiredHeadsetReceiver);
|
apprtcContext.unregisterReceiver(receiver);
|
||||||
wiredHeadsetReceiver = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the speaker phone mode. */
|
/** Sets the speaker phone mode. */
|
||||||
@ -365,55 +440,139 @@ public class AppRTCAudioManager {
|
|||||||
return audioManager.isWiredHeadsetOn();
|
return audioManager.isWiredHeadsetOn();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update list of possible audio devices and make new device selection. */
|
/**
|
||||||
private void updateAudioDeviceState(boolean hasWiredHeadset) {
|
* Updates list of possible audio devices and make new device selection.
|
||||||
// Update the list of available audio devices.
|
* TODO(henrika): add unit test to verify all state transitions.
|
||||||
audioDevices.clear();
|
*/
|
||||||
|
public void updateAudioDeviceState() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
Log.d(TAG, "--- updateAudioDeviceState: "
|
||||||
|
+ "wired headset=" + hasWiredHeadset + ", "
|
||||||
|
+ "BT state=" + bluetoothManager.getState());
|
||||||
|
Log.d(TAG, "Device status: "
|
||||||
|
+ "available=" + audioDevices + ", "
|
||||||
|
+ "selected=" + selectedAudioDevice + ", "
|
||||||
|
+ "user selected=" + userSelectedAudioDevice);
|
||||||
|
|
||||||
|
// Check if any Bluetooth headset is connected. The internal BT state will
|
||||||
|
// change accordingly.
|
||||||
|
// TODO(henrika): perhaps wrap required state into BT manager.
|
||||||
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
||||||
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
|
||||||
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_DISCONNECTING) {
|
||||||
|
bluetoothManager.updateDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the set of available audio devices.
|
||||||
|
Set<AudioDevice> newAudioDevices = new HashSet<>();
|
||||||
|
|
||||||
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|
||||||
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|
||||||
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
|
||||||
|
newAudioDevices.add(AudioDevice.BLUETOOTH);
|
||||||
|
}
|
||||||
|
|
||||||
if (hasWiredHeadset) {
|
if (hasWiredHeadset) {
|
||||||
// If a wired headset is connected, then it is the only possible option.
|
// If a wired headset is connected, then it is the only possible option.
|
||||||
audioDevices.add(AudioDevice.WIRED_HEADSET);
|
newAudioDevices.add(AudioDevice.WIRED_HEADSET);
|
||||||
} else {
|
} else {
|
||||||
// No wired headset, hence the audio-device list can contain speaker
|
// No wired headset, hence the audio-device list can contain speaker
|
||||||
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
||||||
audioDevices.add(AudioDevice.SPEAKER_PHONE);
|
newAudioDevices.add(AudioDevice.SPEAKER_PHONE);
|
||||||
if (hasEarpiece()) {
|
if (hasEarpiece()) {
|
||||||
audioDevices.add(AudioDevice.EARPIECE);
|
newAudioDevices.add(AudioDevice.EARPIECE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "audioDevices: " + audioDevices);
|
// Store state which is set to true if the device list has changed.
|
||||||
|
boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices);
|
||||||
|
// Update the existing audio device set.
|
||||||
|
audioDevices = newAudioDevices;
|
||||||
|
// Correct user selected audio devices if needed.
|
||||||
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
|
||||||
|
&& userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
|
||||||
|
// If BT is not available, it can't be the user selection.
|
||||||
|
userSelectedAudioDevice = AudioDevice.NONE;
|
||||||
|
}
|
||||||
|
if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) {
|
||||||
|
// If user selected speaker phone, but then plugged wired headset then make
|
||||||
|
// wired headset as user selected device.
|
||||||
|
userSelectedAudioDevice = AudioDevice.WIRED_HEADSET;
|
||||||
|
}
|
||||||
|
if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) {
|
||||||
|
// If user selected wired headset, but then unplugged wired headset then make
|
||||||
|
// speaker phone as user selected device.
|
||||||
|
userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE;
|
||||||
|
}
|
||||||
|
|
||||||
// Switch to correct audio device given the list of available audio devices.
|
// Need to start Bluetooth if it is available and user either selected it explicitly or
|
||||||
if (hasWiredHeadset) {
|
// user did not select any output device.
|
||||||
setAudioDevice(AudioDevice.WIRED_HEADSET);
|
boolean needBluetoothAudioStart =
|
||||||
|
bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
||||||
|
&& (userSelectedAudioDevice == AudioDevice.NONE
|
||||||
|
|| userSelectedAudioDevice == AudioDevice.BLUETOOTH);
|
||||||
|
|
||||||
|
// Need to stop Bluetooth audio if user selected different device and
|
||||||
|
// Bluetooth SCO connection is established or in the process.
|
||||||
|
boolean needBluetoothAudioStop =
|
||||||
|
(bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
|
||||||
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
|
||||||
|
&& (userSelectedAudioDevice != AudioDevice.NONE
|
||||||
|
&& userSelectedAudioDevice != AudioDevice.BLUETOOTH);
|
||||||
|
|
||||||
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
|
||||||
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
|
||||||
|
|| bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
|
||||||
|
Log.d(TAG, "Need BT audio: start=" + needBluetoothAudioStart + ", "
|
||||||
|
+ "stop=" + needBluetoothAudioStop + ", "
|
||||||
|
+ "BT state=" + bluetoothManager.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start or stop Bluetooth SCO connection given states set earlier.
|
||||||
|
if (needBluetoothAudioStop) {
|
||||||
|
bluetoothManager.stopScoAudio();
|
||||||
|
bluetoothManager.updateDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needBluetoothAudioStart && !needBluetoothAudioStop) {
|
||||||
|
// Attempt to start Bluetooth SCO audio (takes a few second to start).
|
||||||
|
if (!bluetoothManager.startScoAudio()) {
|
||||||
|
// Remove BLUETOOTH from list of available devices since SCO failed.
|
||||||
|
audioDevices.remove(AudioDevice.BLUETOOTH);
|
||||||
|
audioDeviceSetUpdated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update selected audio device.
|
||||||
|
AudioDevice newAudioDevice = selectedAudioDevice;
|
||||||
|
|
||||||
|
if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
|
||||||
|
// If a Bluetooth is connected, then it should be used as output audio
|
||||||
|
// device. Note that it is not sufficient that a headset is available;
|
||||||
|
// an active SCO channel must also be up and running.
|
||||||
|
newAudioDevice = AudioDevice.BLUETOOTH;
|
||||||
|
} else if (hasWiredHeadset) {
|
||||||
|
// If a wired headset is connected, but Bluetooth is not, then wired headset is used as
|
||||||
|
// audio device.
|
||||||
|
newAudioDevice = AudioDevice.WIRED_HEADSET;
|
||||||
} else {
|
} else {
|
||||||
setAudioDevice(defaultAudioDevice);
|
// No wired headset and no Bluetooth, hence the audio-device list can contain speaker
|
||||||
|
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
||||||
|
// |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE
|
||||||
|
// depending on the user's selection.
|
||||||
|
newAudioDevice = defaultAudioDevice;
|
||||||
|
}
|
||||||
|
// Switch to new device but only if there has been any changes.
|
||||||
|
if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
|
||||||
|
// Do the required device switch.
|
||||||
|
setAudioDeviceInternal(newAudioDevice);
|
||||||
|
Log.d(TAG, "New device status: "
|
||||||
|
+ "available=" + audioDevices + ", "
|
||||||
|
+ "selected=" + newAudioDevice);
|
||||||
|
if (audioManagerEvents != null) {
|
||||||
|
// Notify a listening client that audio device has been changed.
|
||||||
|
audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Log.d(TAG, "--- updateAudioDeviceState done");
|
||||||
/** Called each time a new audio device has been added or removed. */
|
|
||||||
private void onAudioManagerChangedState() {
|
|
||||||
Log.d(TAG, "onAudioManagerChangedState: devices=" + audioDevices + ", selected="
|
|
||||||
+ selectedAudioDevice);
|
|
||||||
|
|
||||||
// Enable the proximity sensor if there are two available audio devices
|
|
||||||
// in the list. Given the current implementation, we know that the choice
|
|
||||||
// will then be between EARPIECE and SPEAKER_PHONE.
|
|
||||||
if (audioDevices.size() == 2) {
|
|
||||||
AppRTCUtils.assertIsTrue(audioDevices.contains(AudioDevice.EARPIECE)
|
|
||||||
&& audioDevices.contains(AudioDevice.SPEAKER_PHONE));
|
|
||||||
// Start the proximity sensor.
|
|
||||||
proximitySensor.start();
|
|
||||||
} else if (audioDevices.size() == 1) {
|
|
||||||
// Stop the proximity sensor since it is no longer needed.
|
|
||||||
proximitySensor.stop();
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Invalid device list");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onStateChangeListener != null) {
|
|
||||||
// Run callback to notify a listening client. The client can then
|
|
||||||
// use public getters to query the new state.
|
|
||||||
onStateChangeListener.run();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,521 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 The WebRTC Project Authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by a BSD-style license
|
||||||
|
* that can be found in the LICENSE file in the root of the source
|
||||||
|
* tree. An additional intellectual property rights grant can be found
|
||||||
|
* in the file PATENTS. All contributing project authors may
|
||||||
|
* be found in the AUTHORS file in the root of the source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.appspot.apprtc;
|
||||||
|
|
||||||
|
import org.appspot.apprtc.util.AppRTCUtils;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.bluetooth.BluetoothHeadset;
|
||||||
|
import android.bluetooth.BluetoothProfile;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Process;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.webrtc.ThreadUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AppRTCProximitySensor manages functions related to Bluetoth devices in the
|
||||||
|
* AppRTC demo.
|
||||||
|
*/
|
||||||
|
public class AppRTCBluetoothManager {
|
||||||
|
private static final String TAG = "AppRTCBluetoothManager";
|
||||||
|
|
||||||
|
// Timeout interval for starting or stopping audio to a Bluetooth SCO device.
|
||||||
|
private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000;
|
||||||
|
// Maximum number of SCO connection attempts.
|
||||||
|
private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2;
|
||||||
|
|
||||||
|
// Bluetooth connection state.
|
||||||
|
public enum State {
|
||||||
|
// Bluetooth is not available; no adapter or Bluetooth is off.
|
||||||
|
UNINITIALIZED,
|
||||||
|
// Bluetooth error happened when trying to start Bluetooth.
|
||||||
|
ERROR,
|
||||||
|
// Bluetooth proxy object for the Headset profile exists, but no connected headset devices,
|
||||||
|
// SCO is not started or disconnected.
|
||||||
|
HEADSET_UNAVAILABLE,
|
||||||
|
// Bluetooth proxy object for the Headset profile connected, connected Bluetooth headset
|
||||||
|
// present, but SCO is not started or disconnected.
|
||||||
|
HEADSET_AVAILABLE,
|
||||||
|
// Bluetooth audio SCO connection with remote device is closing.
|
||||||
|
SCO_DISCONNECTING,
|
||||||
|
// Bluetooth audio SCO connection with remote device is initiated.
|
||||||
|
SCO_CONNECTING,
|
||||||
|
// Bluetooth audio SCO connection with remote device is established.
|
||||||
|
SCO_CONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Context apprtcContext;
|
||||||
|
private final AppRTCAudioManager apprtcAudioManager;
|
||||||
|
private final AudioManager audioManager;
|
||||||
|
private final Handler handler;
|
||||||
|
|
||||||
|
int scoConnectionAttempts;
|
||||||
|
private State bluetoothState;
|
||||||
|
private final BluetoothProfile.ServiceListener bluetoothServiceListener;
|
||||||
|
private BluetoothAdapter bluetoothAdapter;
|
||||||
|
private BluetoothHeadset bluetoothHeadset;
|
||||||
|
private BluetoothDevice bluetoothDevice;
|
||||||
|
private final BroadcastReceiver bluetoothHeadsetReceiver;
|
||||||
|
|
||||||
|
// Runs when the Bluetooth timeout expires. We use that timeout after calling
|
||||||
|
// startScoAudio() or stopScoAudio() because we're not guaranteed to get a
|
||||||
|
// callback after those calls.
|
||||||
|
private final Runnable bluetoothTimeoutRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
bluetoothTimeout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
|
||||||
|
* connected to or disconnected from the service.
|
||||||
|
*/
|
||||||
|
private class BluetoothServiceListener implements BluetoothProfile.ServiceListener {
|
||||||
|
@Override
|
||||||
|
// Called to notify the client when the proxy object has been connected to the service.
|
||||||
|
// Once we have the profile proxy object, we can use it to monitor the state of the
|
||||||
|
// connection and perform other operations that are relevant to the headset profile.
|
||||||
|
public void onServiceConnected(int profile, BluetoothProfile proxy) {
|
||||||
|
if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState);
|
||||||
|
// Android only supports one connected Bluetooth Headset at a time.
|
||||||
|
bluetoothHeadset = (BluetoothHeadset) proxy;
|
||||||
|
updateAudioDeviceState();
|
||||||
|
Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
/** Notifies the client when the proxy object has been disconnected from the service. */
|
||||||
|
public void onServiceDisconnected(int profile) {
|
||||||
|
if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState);
|
||||||
|
stopScoAudio();
|
||||||
|
bluetoothHeadset = null;
|
||||||
|
bluetoothDevice = null;
|
||||||
|
bluetoothState = State.HEADSET_UNAVAILABLE;
|
||||||
|
updateAudioDeviceState();
|
||||||
|
Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intent broadcast receiver which handles changes in Bluetooth device availability.
|
||||||
|
// Detects headset changes and Bluetooth SCO state changes.
|
||||||
|
private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
if (bluetoothState == State.UNINITIALIZED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String action = intent.getAction();
|
||||||
|
// Change in connection state of the Headset profile. Note that the
|
||||||
|
// change does not tell us anything about whether we're streaming
|
||||||
|
// audio to BT over SCO. Typically received when user turns on a BT
|
||||||
|
// headset while audio is active using another audio device.
|
||||||
|
if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
|
||||||
|
final int state =
|
||||||
|
intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED);
|
||||||
|
Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: "
|
||||||
|
+ "a=ACTION_CONNECTION_STATE_CHANGED, "
|
||||||
|
+ "s=" + stateToString(state) + ", "
|
||||||
|
+ "sb=" + isInitialStickyBroadcast() + ", "
|
||||||
|
+ "BT state: " + bluetoothState);
|
||||||
|
if (state == BluetoothHeadset.STATE_CONNECTED) {
|
||||||
|
scoConnectionAttempts = 0;
|
||||||
|
updateAudioDeviceState();
|
||||||
|
} else if (state == BluetoothHeadset.STATE_CONNECTING) {
|
||||||
|
// No action needed.
|
||||||
|
} else if (state == BluetoothHeadset.STATE_DISCONNECTING) {
|
||||||
|
// No action needed.
|
||||||
|
} else if (state == BluetoothHeadset.STATE_DISCONNECTED) {
|
||||||
|
// Bluetooth is probably powered off during the call.
|
||||||
|
stopScoAudio();
|
||||||
|
updateAudioDeviceState();
|
||||||
|
}
|
||||||
|
// Change in the audio (SCO) connection state of the Headset profile.
|
||||||
|
// Typically received after call to startScoAudio() has finalized.
|
||||||
|
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
|
||||||
|
final int state = intent.getIntExtra(
|
||||||
|
BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
|
||||||
|
Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: "
|
||||||
|
+ "a=ACTION_AUDIO_STATE_CHANGED, "
|
||||||
|
+ "s=" + stateToString(state) + ", "
|
||||||
|
+ "sb=" + isInitialStickyBroadcast() + ", "
|
||||||
|
+ "BT state: " + bluetoothState);
|
||||||
|
if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
|
||||||
|
cancelTimer();
|
||||||
|
if (bluetoothState == State.SCO_CONNECTING) {
|
||||||
|
Log.d(TAG, "+++ Bluetooth audio SCO is now connected");
|
||||||
|
bluetoothState = State.SCO_CONNECTED;
|
||||||
|
scoConnectionAttempts = 0;
|
||||||
|
updateAudioDeviceState();
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED");
|
||||||
|
}
|
||||||
|
} else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
|
||||||
|
Log.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
|
||||||
|
} else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
|
||||||
|
Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected");
|
||||||
|
if (isInitialStickyBroadcast()) {
|
||||||
|
Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateAudioDeviceState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(TAG, "onReceive done: BT state=" + bluetoothState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Construction. */
|
||||||
|
static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) {
|
||||||
|
Log.d(TAG, "create" + AppRTCUtils.getThreadInfo());
|
||||||
|
return new AppRTCBluetoothManager(context, audioManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) {
|
||||||
|
Log.d(TAG, "ctor");
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
apprtcContext = context;
|
||||||
|
apprtcAudioManager = audioManager;
|
||||||
|
this.audioManager = getAudioManager(context);
|
||||||
|
bluetoothState = State.UNINITIALIZED;
|
||||||
|
bluetoothServiceListener = new BluetoothServiceListener();
|
||||||
|
bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
|
||||||
|
handler = new Handler(Looper.getMainLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the internal state. */
|
||||||
|
public State getState() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
return bluetoothState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates components required to detect Bluetooth devices and to enable
|
||||||
|
* BT SCO (audio is routed via BT SCO) for the headset profile. The end
|
||||||
|
* state will be HEADSET_UNAVAILABLE but a state machine has started which
|
||||||
|
* will start a state change sequence where the final outcome depends on
|
||||||
|
* if/when the BT headset is enabled.
|
||||||
|
* Example of state change sequence when start() is called while BT device
|
||||||
|
* is connected and enabled:
|
||||||
|
* 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() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
Log.d(TAG, "start");
|
||||||
|
if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
|
||||||
|
Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bluetoothState != State.UNINITIALIZED) {
|
||||||
|
Log.w(TAG, "Invalid BT state");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bluetoothHeadset = null;
|
||||||
|
bluetoothDevice = null;
|
||||||
|
scoConnectionAttempts = 0;
|
||||||
|
// Get a handle to the default local Bluetooth adapter.
|
||||||
|
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
if (bluetoothAdapter == null) {
|
||||||
|
Log.w(TAG, "Device does not support Bluetooth");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ensure that the device supports use of BT SCO audio for off call use cases.
|
||||||
|
if (!audioManager.isBluetoothScoAvailableOffCall()) {
|
||||||
|
Log.e(TAG, "Bluetooth SCO audio is not available off call");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logBluetoothAdapterInfo(bluetoothAdapter);
|
||||||
|
// Establish a connection to the HEADSET profile (includes both Bluetooth Headset and
|
||||||
|
// Hands-Free) proxy object and install a listener.
|
||||||
|
if (!getBluetoothProfileProxy(
|
||||||
|
apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
|
||||||
|
Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Register receivers for BluetoothHeadset change notifications.
|
||||||
|
IntentFilter bluetoothHeadsetFilter = new IntentFilter();
|
||||||
|
// Register receiver for change in connection state of the Headset profile.
|
||||||
|
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
|
||||||
|
// Register receiver for change in audio connection state of the Headset profile.
|
||||||
|
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
|
||||||
|
registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter);
|
||||||
|
Log.d(TAG, "HEADSET profile state: "
|
||||||
|
+ stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
|
||||||
|
Log.d(TAG, "Bluetooth proxy for headset profile has started");
|
||||||
|
bluetoothState = State.HEADSET_UNAVAILABLE;
|
||||||
|
Log.d(TAG, "start done: BT state=" + bluetoothState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stops and closes all components related to Bluetooth audio. */
|
||||||
|
public void stop() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
unregisterReceiver(bluetoothHeadsetReceiver);
|
||||||
|
Log.d(TAG, "stop: BT state=" + bluetoothState);
|
||||||
|
if (bluetoothAdapter != null) {
|
||||||
|
// Stop BT SCO connection with remote device if needed.
|
||||||
|
stopScoAudio();
|
||||||
|
// Close down remaining BT resources.
|
||||||
|
if (bluetoothState != State.UNINITIALIZED) {
|
||||||
|
cancelTimer();
|
||||||
|
if (bluetoothHeadset != null) {
|
||||||
|
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
|
||||||
|
bluetoothHeadset = null;
|
||||||
|
}
|
||||||
|
bluetoothAdapter = null;
|
||||||
|
bluetoothDevice = null;
|
||||||
|
bluetoothState = State.UNINITIALIZED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(TAG, "stop done: BT state=" + bluetoothState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts Bluetooth SCO connection with remote device.
|
||||||
|
* Note that the phone application always has the priority on the usage of the SCO connection
|
||||||
|
* for telephony. If this method is called while the phone is in call it will be ignored.
|
||||||
|
* Similarly, if a call is received or sent while an application is using the SCO connection,
|
||||||
|
* the connection will be lost for the application and NOT returned automatically when the call
|
||||||
|
* ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a
|
||||||
|
* virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO
|
||||||
|
* audio connection is established.
|
||||||
|
* 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
|
||||||
|
* accept SCO audio without a "call".
|
||||||
|
*/
|
||||||
|
public boolean startScoAudio() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
Log.d(TAG, "startSco: BT state=" + bluetoothState + ", "
|
||||||
|
+ "attempts: " + scoConnectionAttempts + ", "
|
||||||
|
+ "SCO is on: " + isScoOn());
|
||||||
|
if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
|
||||||
|
Log.e(TAG, "BT SCO connection fails - no more attempts");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (bluetoothState != State.HEADSET_AVAILABLE) {
|
||||||
|
Log.e(TAG, "BT SCO connection fails - no headset available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
|
||||||
|
Log.d(TAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED...");
|
||||||
|
// 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
|
||||||
|
// intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED.
|
||||||
|
bluetoothState = State.SCO_CONNECTING;
|
||||||
|
audioManager.startBluetoothSco();
|
||||||
|
scoConnectionAttempts++;
|
||||||
|
startTimer();
|
||||||
|
Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stops Bluetooth SCO connection with remote device. */
|
||||||
|
public void stopScoAudio() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", "
|
||||||
|
+ "SCO is on: " + isScoOn());
|
||||||
|
if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelTimer();
|
||||||
|
audioManager.stopBluetoothSco();
|
||||||
|
bluetoothState = State.SCO_DISCONNECTING;
|
||||||
|
Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the BluetoothHeadset proxy object (controls the Bluetooth Headset
|
||||||
|
* Service via IPC) to update the list of connected devices for the HEADSET
|
||||||
|
* profile. The internal state will change to HEADSET_UNAVAILABLE or to
|
||||||
|
* HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected
|
||||||
|
* device if available.
|
||||||
|
*/
|
||||||
|
public void updateDevice() {
|
||||||
|
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "updateDevice");
|
||||||
|
// Get connected devices for the headset profile. Returns the set of
|
||||||
|
// devices which are in state STATE_CONNECTED. The BluetoothDevice class
|
||||||
|
// is just a thin wrapper for a Bluetooth hardware address.
|
||||||
|
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
|
||||||
|
if (devices.isEmpty()) {
|
||||||
|
bluetoothDevice = null;
|
||||||
|
bluetoothState = State.HEADSET_UNAVAILABLE;
|
||||||
|
Log.d(TAG, "No connected bluetooth headset");
|
||||||
|
} else {
|
||||||
|
// Always use first device is list. Android only supports one device.
|
||||||
|
bluetoothDevice = devices.get(0);
|
||||||
|
bluetoothState = State.HEADSET_AVAILABLE;
|
||||||
|
Log.d(TAG, "Connected bluetooth headset: "
|
||||||
|
+ "name=" + bluetoothDevice.getName() + ", "
|
||||||
|
+ "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
|
||||||
|
+ ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
|
||||||
|
}
|
||||||
|
Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stubs for test mocks.
|
||||||
|
*/
|
||||||
|
protected AudioManager getAudioManager(Context context) {
|
||||||
|
return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
|
||||||
|
apprtcContext.registerReceiver(receiver, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void unregisterReceiver(BroadcastReceiver receiver) {
|
||||||
|
apprtcContext.unregisterReceiver(receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean getBluetoothProfileProxy(
|
||||||
|
Context context, BluetoothProfile.ServiceListener listener, int profile) {
|
||||||
|
return bluetoothAdapter.getProfileProxy(context, listener, profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean hasPermission(Context context, String permission) {
|
||||||
|
return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid())
|
||||||
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logs the state of the local Bluetooth adapter. */
|
||||||
|
protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
|
||||||
|
Log.d(TAG, "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(TAG, "paired devices:");
|
||||||
|
for (BluetoothDevice device : pairedDevices) {
|
||||||
|
Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ensures that the audio manager updates its list of available audio devices. */
|
||||||
|
private void updateAudioDeviceState() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
Log.d(TAG, "updateAudioDeviceState");
|
||||||
|
apprtcAudioManager.updateAudioDeviceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. */
|
||||||
|
private void startTimer() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
Log.d(TAG, "startTimer");
|
||||||
|
handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancels any outstanding timer tasks. */
|
||||||
|
private void cancelTimer() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
Log.d(TAG, "cancelTimer");
|
||||||
|
handler.removeCallbacks(bluetoothTimeoutRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when start of the BT SCO channel takes too long time. Usually
|
||||||
|
* happens when the BT device has been turned on during an ongoing call.
|
||||||
|
*/
|
||||||
|
private void bluetoothTimeout() {
|
||||||
|
ThreadUtils.checkIsOnMainThread();
|
||||||
|
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "
|
||||||
|
+ "attempts: " + scoConnectionAttempts + ", "
|
||||||
|
+ "SCO is on: " + isScoOn());
|
||||||
|
if (bluetoothState != State.SCO_CONNECTING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Bluetooth SCO should be connecting; check the latest result.
|
||||||
|
boolean scoConnected = false;
|
||||||
|
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
|
||||||
|
if (devices.size() > 0) {
|
||||||
|
bluetoothDevice = devices.get(0);
|
||||||
|
if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
|
||||||
|
Log.d(TAG, "SCO connected with " + bluetoothDevice.getName());
|
||||||
|
scoConnected = true;
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scoConnected) {
|
||||||
|
// We thought BT had timed out, but it's actually on; updating state.
|
||||||
|
bluetoothState = State.SCO_CONNECTED;
|
||||||
|
scoConnectionAttempts = 0;
|
||||||
|
} else {
|
||||||
|
// Give up and "cancel" our request by calling stopBluetoothSco().
|
||||||
|
Log.w(TAG, "BT failed to connect after timeout");
|
||||||
|
stopScoAudio();
|
||||||
|
}
|
||||||
|
updateAudioDeviceState();
|
||||||
|
Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks whether audio uses Bluetooth SCO. */
|
||||||
|
private boolean isScoOn() {
|
||||||
|
return audioManager.isBluetoothScoOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts BluetoothAdapter states into local string representations. */
|
||||||
|
private String stateToString(int state) {
|
||||||
|
switch (state) {
|
||||||
|
case BluetoothAdapter.STATE_DISCONNECTED:
|
||||||
|
return "DISCONNECTED";
|
||||||
|
case BluetoothAdapter.STATE_CONNECTED:
|
||||||
|
return "CONNECTED";
|
||||||
|
case BluetoothAdapter.STATE_CONNECTING:
|
||||||
|
return "CONNECTING";
|
||||||
|
case BluetoothAdapter.STATE_DISCONNECTING:
|
||||||
|
return "DISCONNECTING";
|
||||||
|
case BluetoothAdapter.STATE_OFF:
|
||||||
|
return "OFF";
|
||||||
|
case BluetoothAdapter.STATE_ON:
|
||||||
|
return "ON";
|
||||||
|
case BluetoothAdapter.STATE_TURNING_OFF:
|
||||||
|
// Indicates the local Bluetooth adapter is turning off. Local clients should immediately
|
||||||
|
// attempt graceful disconnection of any remote links.
|
||||||
|
return "TURNING_OFF";
|
||||||
|
case BluetoothAdapter.STATE_TURNING_ON:
|
||||||
|
// Indicates the local Bluetooth adapter is turning on. However local clients should wait
|
||||||
|
// for STATE_ON before attempting to use the adapter.
|
||||||
|
return "TURNING_ON";
|
||||||
|
default:
|
||||||
|
return "INVALID";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -33,6 +33,9 @@ import java.io.IOException;
|
|||||||
import java.lang.RuntimeException;
|
import java.lang.RuntimeException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.appspot.apprtc.AppRTCAudioManager.AudioDevice;
|
||||||
|
import org.appspot.apprtc.AppRTCAudioManager.AudioManagerEvents;
|
||||||
import org.appspot.apprtc.AppRTCClient.RoomConnectionParameters;
|
import org.appspot.apprtc.AppRTCClient.RoomConnectionParameters;
|
||||||
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
|
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
|
||||||
import org.appspot.apprtc.PeerConnectionClient.DataChannelParameters;
|
import org.appspot.apprtc.PeerConnectionClient.DataChannelParameters;
|
||||||
@ -527,18 +530,19 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
|
|||||||
|
|
||||||
// Create and audio manager that will take care of audio routing,
|
// Create and audio manager that will take care of audio routing,
|
||||||
// audio modes, audio device enumeration etc.
|
// audio modes, audio device enumeration etc.
|
||||||
audioManager = AppRTCAudioManager.create(this, new Runnable() {
|
audioManager = AppRTCAudioManager.create(this);
|
||||||
// This method will be called each time the audio state (number and
|
|
||||||
// type of devices) has been changed.
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
onAudioManagerChangedState();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Store existing audio settings and change audio mode to
|
// Store existing audio settings and change audio mode to
|
||||||
// MODE_IN_COMMUNICATION for best possible VoIP performance.
|
// MODE_IN_COMMUNICATION for best possible VoIP performance.
|
||||||
Log.d(TAG, "Initializing the audio manager...");
|
Log.d(TAG, "Starting the audio manager...");
|
||||||
audioManager.init();
|
audioManager.start(new AudioManagerEvents() {
|
||||||
|
// This method will be called each time the number of available audio
|
||||||
|
// devices has changed.
|
||||||
|
@Override
|
||||||
|
public void onAudioDeviceChanged(
|
||||||
|
AudioDevice audioDevice, Set<AudioDevice> availableAudioDevices) {
|
||||||
|
onAudioManagerDevicesChanged(audioDevice, availableAudioDevices);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should be called from UI thread
|
// Should be called from UI thread
|
||||||
@ -555,9 +559,13 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
|
|||||||
peerConnectionClient.enableStatsEvents(true, STAT_CALLBACK_PERIOD);
|
peerConnectionClient.enableStatsEvents(true, STAT_CALLBACK_PERIOD);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onAudioManagerChangedState() {
|
// This method is called when the audio manager reports audio device change,
|
||||||
// TODO(henrika): disable video if AppRTCAudioManager.AudioDevice.EARPIECE
|
// e.g. from wired headset to speakerphone.
|
||||||
// is active.
|
private void onAudioManagerDevicesChanged(
|
||||||
|
final AudioDevice device, final Set<AudioDevice> availableDevices) {
|
||||||
|
Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", "
|
||||||
|
+ "selected: " + device);
|
||||||
|
// TODO(henrika): add callback handler.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect from remote resources, dispose of local resources, and exit.
|
// Disconnect from remote resources, dispose of local resources, and exit.
|
||||||
@ -584,7 +592,7 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
|
|||||||
remoteRenderScreen = null;
|
remoteRenderScreen = null;
|
||||||
}
|
}
|
||||||
if (audioManager != null) {
|
if (audioManager != null) {
|
||||||
audioManager.close();
|
audioManager.stop();
|
||||||
audioManager = null;
|
audioManager = null;
|
||||||
}
|
}
|
||||||
if (iceConnected && !isError) {
|
if (iceConnected && !isError) {
|
||||||
|
|||||||
@ -0,0 +1,271 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 The WebRTC Project Authors. All rights reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by a BSD-style license
|
||||||
|
* that can be found in the LICENSE file in the root of the source
|
||||||
|
* tree. An additional intellectual property rights grant can be found
|
||||||
|
* in the file PATENTS. All contributing project authors may
|
||||||
|
* be found in the AUTHORS file in the root of the source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.appspot.apprtc;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.doCallRealMethod;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.bluetooth.BluetoothHeadset;
|
||||||
|
import android.bluetooth.BluetoothProfile;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.util.Log;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.appspot.apprtc.AppRTCBluetoothManager.State;
|
||||||
|
import org.chromium.testing.local.LocalRobolectricTestRunner;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.annotation.Config;
|
||||||
|
import org.robolectric.shadows.ShadowApplication;
|
||||||
|
import org.robolectric.shadows.ShadowLog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies basic behavior of the AppRTCBluetoothManager class.
|
||||||
|
* Note that the test object uses an AppRTCAudioManager (injected in ctor),
|
||||||
|
* but a mocked version is used instead. Hence, the parts "driven" by the AppRTC
|
||||||
|
* audio manager are not included in this test.
|
||||||
|
*/
|
||||||
|
@RunWith(LocalRobolectricTestRunner.class)
|
||||||
|
@Config(manifest = Config.NONE)
|
||||||
|
public class BluetoothManagerTest {
|
||||||
|
private static final String TAG = "BluetoothManagerTest";
|
||||||
|
private static final String BLUETOOTH_TEST_DEVICE_NAME = "BluetoothTestDevice";
|
||||||
|
|
||||||
|
private BroadcastReceiver bluetoothHeadsetStateReceiver;
|
||||||
|
private BluetoothProfile.ServiceListener bluetoothServiceListener;
|
||||||
|
private BluetoothHeadset mockedBluetoothHeadset;
|
||||||
|
private BluetoothDevice mockedBluetoothDevice;
|
||||||
|
private List<BluetoothDevice> mockedBluetoothDeviceList;
|
||||||
|
private AppRTCBluetoothManager bluetoothManager;
|
||||||
|
private AppRTCAudioManager mockedAppRtcAudioManager;
|
||||||
|
private AudioManager mockedAudioManager;
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
ShadowLog.stream = System.out;
|
||||||
|
context = ShadowApplication.getInstance().getApplicationContext();
|
||||||
|
mockedAppRtcAudioManager = mock(AppRTCAudioManager.class);
|
||||||
|
mockedAudioManager = mock(AudioManager.class);
|
||||||
|
mockedBluetoothHeadset = mock(BluetoothHeadset.class);
|
||||||
|
mockedBluetoothDevice = mock(BluetoothDevice.class);
|
||||||
|
mockedBluetoothDeviceList = new LinkedList<BluetoothDevice>();
|
||||||
|
|
||||||
|
// Simulate that bluetooth SCO audio is available by default.
|
||||||
|
when(mockedAudioManager.isBluetoothScoAvailableOffCall()).thenReturn(true);
|
||||||
|
|
||||||
|
// Create the test object and override protected methods for this test.
|
||||||
|
bluetoothManager = new AppRTCBluetoothManager(context, mockedAppRtcAudioManager) {
|
||||||
|
@Override
|
||||||
|
protected AudioManager getAudioManager(Context context) {
|
||||||
|
Log.d(TAG, "getAudioManager");
|
||||||
|
return mockedAudioManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
|
||||||
|
Log.d(TAG, "registerReceiver");
|
||||||
|
if (filter.hasAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
|
||||||
|
&& filter.hasAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
|
||||||
|
// Gives access to the real broadcast receiver so the test can use it.
|
||||||
|
bluetoothHeadsetStateReceiver = receiver;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void unregisterReceiver(BroadcastReceiver receiver) {
|
||||||
|
Log.d(TAG, "unregisterReceiver");
|
||||||
|
if (receiver == bluetoothHeadsetStateReceiver) {
|
||||||
|
bluetoothHeadsetStateReceiver = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean getBluetoothProfileProxy(
|
||||||
|
Context context, BluetoothProfile.ServiceListener listener, int profile) {
|
||||||
|
Log.d(TAG, "getBluetoothProfileProxy");
|
||||||
|
if (profile == BluetoothProfile.HEADSET) {
|
||||||
|
// Allows the test to access the real Bluetooth service listener object.
|
||||||
|
bluetoothServiceListener = listener;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean hasPermission(Context context, String permission) {
|
||||||
|
Log.d(TAG, "hasPermission(" + permission + ")");
|
||||||
|
// Ensure that the client asks for Bluetooth permission.
|
||||||
|
return (permission == android.Manifest.permission.BLUETOOTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
|
||||||
|
// Do nothing in tests. No need to mock BluetoothAdapter.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that Bluetooth service listener for headset profile is properly initialized.
|
||||||
|
@Test
|
||||||
|
public void testBluetoothServiceListenerInitialized() {
|
||||||
|
bluetoothManager.start();
|
||||||
|
assertNotNull(bluetoothServiceListener);
|
||||||
|
verify(mockedAppRtcAudioManager, never()).updateAudioDeviceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that broadcast receivers for Bluetooth SCO audio state and Bluetooth headset state
|
||||||
|
// are properly registered and unregistered.
|
||||||
|
@Test
|
||||||
|
public void testBluetoothBroadcastReceiversAreRegistered() {
|
||||||
|
bluetoothManager.start();
|
||||||
|
assertNotNull(bluetoothHeadsetStateReceiver);
|
||||||
|
bluetoothManager.stop();
|
||||||
|
assertNull(bluetoothHeadsetStateReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the Bluetooth manager starts and stops with correct states.
|
||||||
|
@Test
|
||||||
|
public void testBluetoothDefaultStartStopStates() {
|
||||||
|
bluetoothManager.start();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
|
||||||
|
bluetoothManager.stop();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.UNINITIALIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify correct state after receiving BluetoothServiceListener.onServiceConnected()
|
||||||
|
// when no BT device is enabled.
|
||||||
|
@Test
|
||||||
|
public void testBluetoothServiceListenerConnectedWithNoHeadset() {
|
||||||
|
bluetoothManager.start();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
|
||||||
|
simulateBluetoothServiceConnectedWithNoConnectedHeadset();
|
||||||
|
verify(mockedAppRtcAudioManager, times(1)).updateAudioDeviceState();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify correct state after receiving BluetoothServiceListener.onServiceConnected()
|
||||||
|
// when one emulated (test) BT device is enabled. Android does not support more than
|
||||||
|
// one connected BT headset.
|
||||||
|
@Test
|
||||||
|
public void testBluetoothServiceListenerConnectedWithHeadset() {
|
||||||
|
bluetoothManager.start();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
|
||||||
|
simulateBluetoothServiceConnectedWithConnectedHeadset();
|
||||||
|
verify(mockedAppRtcAudioManager, times(1)).updateAudioDeviceState();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.HEADSET_AVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify correct state after receiving BluetoothProfile.ServiceListener.onServiceDisconnected().
|
||||||
|
@Test
|
||||||
|
public void testBluetoothServiceListenerDisconnected() {
|
||||||
|
bluetoothManager.start();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
|
||||||
|
simulateBluetoothServiceDisconnected();
|
||||||
|
verify(mockedAppRtcAudioManager, times(1)).updateAudioDeviceState();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify correct state after BluetoothServiceListener.onServiceConnected() and
|
||||||
|
// the intent indicating that the headset is actually connected. Both these callbacks
|
||||||
|
// results in calls to updateAudioDeviceState() on the AppRTC audio manager.
|
||||||
|
// No BT SCO is enabled here to keep the test limited.
|
||||||
|
@Test
|
||||||
|
public void testBluetoothHeadsetConnected() {
|
||||||
|
bluetoothManager.start();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
|
||||||
|
simulateBluetoothServiceConnectedWithConnectedHeadset();
|
||||||
|
simulateBluetoothHeadsetConnected();
|
||||||
|
verify(mockedAppRtcAudioManager, times(2)).updateAudioDeviceState();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.HEADSET_AVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify correct state sequence for a case when a BT headset is available,
|
||||||
|
// followed by BT SCO audio being enabled and then stopped.
|
||||||
|
@Test
|
||||||
|
public void testBluetoothScoAudioStartAndStop() {
|
||||||
|
bluetoothManager.start();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.HEADSET_UNAVAILABLE);
|
||||||
|
simulateBluetoothServiceConnectedWithConnectedHeadset();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.HEADSET_AVAILABLE);
|
||||||
|
bluetoothManager.startScoAudio();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.SCO_CONNECTING);
|
||||||
|
simulateBluetoothScoConnectionConnected();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.SCO_CONNECTED);
|
||||||
|
bluetoothManager.stopScoAudio();
|
||||||
|
simulateBluetoothScoConnectionDisconnected();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.SCO_DISCONNECTING);
|
||||||
|
bluetoothManager.stop();
|
||||||
|
assertEquals(bluetoothManager.getState(), State.UNINITIALIZED);
|
||||||
|
verify(mockedAppRtcAudioManager, times(3)).updateAudioDeviceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private helper methods.
|
||||||
|
*/
|
||||||
|
private void simulateBluetoothServiceConnectedWithNoConnectedHeadset() {
|
||||||
|
mockedBluetoothDeviceList.clear();
|
||||||
|
when(mockedBluetoothHeadset.getConnectedDevices()).thenReturn(mockedBluetoothDeviceList);
|
||||||
|
bluetoothServiceListener.onServiceConnected(BluetoothProfile.HEADSET, mockedBluetoothHeadset);
|
||||||
|
// In real life, the AppRTC audio manager makes this call.
|
||||||
|
bluetoothManager.updateDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void simulateBluetoothServiceConnectedWithConnectedHeadset() {
|
||||||
|
mockedBluetoothDeviceList.clear();
|
||||||
|
mockedBluetoothDeviceList.add(mockedBluetoothDevice);
|
||||||
|
when(mockedBluetoothHeadset.getConnectedDevices()).thenReturn(mockedBluetoothDeviceList);
|
||||||
|
when(mockedBluetoothDevice.getName()).thenReturn(BLUETOOTH_TEST_DEVICE_NAME);
|
||||||
|
bluetoothServiceListener.onServiceConnected(BluetoothProfile.HEADSET, mockedBluetoothHeadset);
|
||||||
|
// In real life, the AppRTC audio manager makes this call.
|
||||||
|
bluetoothManager.updateDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void simulateBluetoothServiceDisconnected() {
|
||||||
|
bluetoothServiceListener.onServiceDisconnected(BluetoothProfile.HEADSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void simulateBluetoothHeadsetConnected() {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
|
||||||
|
intent.putExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_CONNECTED);
|
||||||
|
bluetoothHeadsetStateReceiver.onReceive(context, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void simulateBluetoothScoConnectionConnected() {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
|
||||||
|
intent.putExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_CONNECTED);
|
||||||
|
bluetoothHeadsetStateReceiver.onReceive(context, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void simulateBluetoothScoConnectionDisconnected() {
|
||||||
|
Intent intent = new Intent();
|
||||||
|
intent.setAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
|
||||||
|
intent.putExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
|
||||||
|
bluetoothHeadsetStateReceiver.onReceive(context, intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user