Add support for audio device selection in AppRTCDemo.
Summary: - Creates a list of available (possible to select) audio devices. - Automatically selects (routes audio) the "best/default" audio device. - If possible, starts a proximity sensor that will switch between headset earpiece and speaker phone based on how close the a person's ear the mobile device is held. TBR=glaznev BUG=4103,4109 Review URL: https://webrtc-codereview.appspot.com/31239004 git-svn-id: http://webrtc.googlecode.com/svn/trunk@7978 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
@@ -27,32 +27,107 @@
|
||||
|
||||
package org.appspot.apprtc;
|
||||
|
||||
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.util.Log;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.appspot.apprtc.util.AppRTCUtils;
|
||||
|
||||
/**
|
||||
* AppRTCAudioManager manages all audio related parts of the AppRTC demo.
|
||||
* TODO(henrika): add support for device enumeration, device selection etc.
|
||||
*/
|
||||
public class AppRTCAudioManager {
|
||||
private static final String TAG = "AppRTCAudioManager";
|
||||
|
||||
// Names of possible audio devices that we currently support.
|
||||
// TODO(henrika): add support for BLUETOOTH as well.
|
||||
public enum AudioDevice {
|
||||
SPEAKER_PHONE,
|
||||
WIRED_HEADSET,
|
||||
EARPIECE,
|
||||
}
|
||||
|
||||
private final Context apprtcContext;
|
||||
private final Runnable onStateChangeListener;
|
||||
private boolean initialized = false;
|
||||
private AudioManager audioManager;
|
||||
private int savedAudioMode = AudioManager.MODE_INVALID;
|
||||
private boolean savedIsSpeakerPhoneOn = false;
|
||||
private boolean savedIsMicrophoneMute = false;
|
||||
|
||||
/** Construction */
|
||||
static AppRTCAudioManager create(Context context) {
|
||||
return new AppRTCAudioManager(context);
|
||||
// For now; always use the speaker phone as default device selection when
|
||||
// there is a choice between SPEAKER_PHONE and EARPIECE.
|
||||
// TODO(henrika): it is possible that EARPIECE should be preferred in some
|
||||
// cases. If so, we should set this value at construction instead.
|
||||
private final AudioDevice defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
|
||||
|
||||
// Proximity sensor object. It measures the proximity of an object in cm
|
||||
// relative to the view screen of a device and can therefore be used to
|
||||
// assist device switching (close to ear <=> use headset earpiece if
|
||||
// available, far from ear <=> use speaker phone).
|
||||
private AppRTCProximitySensor proximitySensor = null;
|
||||
|
||||
// Contains the currently selected audio device.
|
||||
private AudioDevice selectedAudioDevice;
|
||||
|
||||
// Contains a list of available audio devices. A Set collection is used to
|
||||
// avoid duplicate elements.
|
||||
private final Set<AudioDevice> audioDevices = new HashSet<AudioDevice>();
|
||||
|
||||
// Broadcast receiver for wired headset intent broadcasts.
|
||||
private BroadcastReceiver wiredHeadsetReceiver;
|
||||
|
||||
// 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() {
|
||||
// The proximity sensor should only be activated when there are exactly two
|
||||
// available audio devices.
|
||||
if (audioDevices.size() == 2 &&
|
||||
audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE) &&
|
||||
audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
|
||||
if (proximitySensor.sensorReportsNearState()) {
|
||||
// Sensor reports that a "handset is being held up to a person's ear",
|
||||
// or "something is covering the light sensor".
|
||||
setAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
|
||||
} else {
|
||||
// Sensor reports that a "handset is removed from a person's ear", or
|
||||
// "the light sensor is no longer covered".
|
||||
setAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AppRTCAudioManager(Context context) {
|
||||
Log.d(TAG, "AppRTCAudioManager");
|
||||
/** Construction */
|
||||
static AppRTCAudioManager create(Context context,
|
||||
Runnable deviceStateChangeListener) {
|
||||
return new AppRTCAudioManager(context, deviceStateChangeListener);
|
||||
}
|
||||
|
||||
private AppRTCAudioManager(Context context,
|
||||
Runnable deviceStateChangeListener) {
|
||||
apprtcContext = context;
|
||||
onStateChangeListener = deviceStateChangeListener;
|
||||
audioManager = ((AudioManager) context.getSystemService(
|
||||
Context.AUDIO_SERVICE));
|
||||
|
||||
// Create and initialize the proximity sensor.
|
||||
// Tablet devices (e.g. Nexus 7) does not support proximity sensors.
|
||||
// Note that, the sensor will not be active until start() has been called.
|
||||
proximitySensor = AppRTCProximitySensor.create(context, new Runnable() {
|
||||
// This method will be called each time a state change is detected.
|
||||
// Example: user holds his hand over the device (closer than ~5 cm),
|
||||
// or removes his hand from the device.
|
||||
public void run() {
|
||||
onProximitySensorChangedState();
|
||||
}
|
||||
});
|
||||
AppRTCUtils.logDeviceInfo(TAG);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
@@ -66,11 +141,27 @@ public class AppRTCAudioManager {
|
||||
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
|
||||
savedIsMicrophoneMute = audioManager.isMicrophoneMute();
|
||||
|
||||
// Request audio focus before making any device switch.
|
||||
audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL,
|
||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
|
||||
|
||||
// The AppRTC demo shall always run in COMMUNICATION mode since it will
|
||||
// result in best possible "VoIP settings", like audio routing, volume
|
||||
// control etc.
|
||||
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
||||
|
||||
// Always disable microphone mute during a WebRTC call.
|
||||
setMicrophoneMute(false);
|
||||
|
||||
// Do initial selection of audio device. This setting can later be changed
|
||||
// either by adding/removing a wired headset or by covering/uncovering the
|
||||
// proximity sensor.
|
||||
updateAudioDeviceState(hasWiredHeadset());
|
||||
|
||||
// Register receiver for broadcast intents related to adding/removing a
|
||||
// wired headset (Intent.ACTION_HEADSET_PLUG).
|
||||
registerForWiredHeadsetIntentBroadcast();
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
@@ -80,14 +171,111 @@ public class AppRTCAudioManager {
|
||||
return;
|
||||
}
|
||||
|
||||
unregisterForWiredHeadsetIntentBroadcast();
|
||||
|
||||
// Restore previously stored audio states.
|
||||
setSpeakerphoneOn(savedIsSpeakerPhoneOn);
|
||||
setMicrophoneMute(savedIsMicrophoneMute);
|
||||
audioManager.setMode(savedAudioMode);
|
||||
audioManager.abandonAudioFocus(null);
|
||||
|
||||
if (proximitySensor != null) {
|
||||
proximitySensor.stop();
|
||||
proximitySensor = null;
|
||||
}
|
||||
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
/** Changes selection of the currently active audio device. */
|
||||
public void setAudioDevice(AudioDevice device) {
|
||||
Log.d(TAG, "setAudioDevice(device=" + device + ")");
|
||||
AppRTCUtils.assertIsTrue(audioDevices.contains(device));
|
||||
|
||||
switch (device) {
|
||||
case SPEAKER_PHONE:
|
||||
setSpeakerphoneOn(true);
|
||||
selectedAudioDevice = AudioDevice.SPEAKER_PHONE;
|
||||
break;
|
||||
case EARPIECE:
|
||||
setSpeakerphoneOn(false);
|
||||
selectedAudioDevice = AudioDevice.EARPIECE;
|
||||
break;
|
||||
case WIRED_HEADSET:
|
||||
setSpeakerphoneOn(false);
|
||||
selectedAudioDevice = AudioDevice.WIRED_HEADSET;
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Invalid audio device selection");
|
||||
break;
|
||||
}
|
||||
onAudioManagerChangedState();
|
||||
}
|
||||
|
||||
/** Returns current set of available/selectable audio devices. */
|
||||
public Set<AudioDevice> getAudioDevices() {
|
||||
return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices));
|
||||
}
|
||||
|
||||
/** Returns the currently selected audio device. */
|
||||
public AudioDevice getSelectedAudioDevice() {
|
||||
return selectedAudioDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers receiver for the broadcasted intent when a wired headset is
|
||||
* plugged in or unplugged. The received intent will have an extra
|
||||
* '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) ? true : false;
|
||||
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. */
|
||||
private void unregisterForWiredHeadsetIntentBroadcast() {
|
||||
apprtcContext.unregisterReceiver(wiredHeadsetReceiver);
|
||||
wiredHeadsetReceiver = null;
|
||||
}
|
||||
|
||||
/** Sets the speaker phone mode. */
|
||||
private void setSpeakerphoneOn(boolean on) {
|
||||
boolean wasOn = audioManager.isSpeakerphoneOn();
|
||||
@@ -105,4 +293,74 @@ public class AppRTCAudioManager {
|
||||
}
|
||||
audioManager.setMicrophoneMute(on);
|
||||
}
|
||||
|
||||
/** Gets the current earpiece state. */
|
||||
private boolean hasEarpiece() {
|
||||
return apprtcContext.getPackageManager().hasSystemFeature(
|
||||
PackageManager.FEATURE_TELEPHONY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a wired headset is connected or not.
|
||||
* This is not a valid indication that audio playback is actually over
|
||||
* the wired headset as audio routing depends on other conditions. We
|
||||
* only use it as an early indicator (during initialization) of an attached
|
||||
* wired headset.
|
||||
*/
|
||||
@Deprecated
|
||||
private boolean hasWiredHeadset() {
|
||||
return audioManager.isWiredHeadsetOn();
|
||||
}
|
||||
|
||||
/** Update list of possible audio devices and make new device selection. */
|
||||
private void updateAudioDeviceState(boolean hasWiredHeadset) {
|
||||
// Update the list of available audio devices.
|
||||
audioDevices.clear();
|
||||
if (hasWiredHeadset) {
|
||||
// If a wired headset is connected, then it is the only possible option.
|
||||
audioDevices.add(AudioDevice.WIRED_HEADSET);
|
||||
} else {
|
||||
// No wired headset, hence the audio-device list can contain speaker
|
||||
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
|
||||
audioDevices.add(AudioDevice.SPEAKER_PHONE);
|
||||
if (hasEarpiece()) {
|
||||
audioDevices.add(AudioDevice.EARPIECE);
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "audioDevices: " + audioDevices);
|
||||
|
||||
// Switch to correct audio device given the list of available audio devices.
|
||||
if (hasWiredHeadset) {
|
||||
setAudioDevice(AudioDevice.WIRED_HEADSET);
|
||||
} else {
|
||||
setAudioDevice(defaultAudioDevice);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user