/*
* libjingle
* Copyright 2014 Google Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* TeleStax, Open Source Cloud Communications
* Copyright 2011-2015, Telestax Inc and individual contributors
* by the @authors tag.
*
* This program is free software: you can redistribute it and/or modify
* under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
* For questions related to commercial use licensing, please contact sales@telestax.com.
*
*/
package org.restcomm.android.sdk.MediaClient;
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 org.appspot.apprtc.util.AppRTCUtils;
import org.restcomm.android.sdk.R;
import org.restcomm.android.sdk.RCDevice;
import org.restcomm.android.sdk.util.RCLogger;
import org.restcomm.android.sdk.MediaClient.util.AppRTCUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
/**
* AppRTCAudioManager manages all audio related parts of the AppRTC demo.
*/
public class AppRTCAudioManager implements AudioManager.OnAudioFocusChangeListener {
private static final String TAG = "AppRTCAudioManager";
/**
* AudioDevice is the 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 boolean callAudioInitialized = false;
private AudioManager audioManager;
private int savedAudioMode = AudioManager.MODE_INVALID;
private boolean savedIsSpeakerPhoneOn = false;
private boolean savedIsMicrophoneMute = false;
private HashMap<String, Integer> resourceIds;
// 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;
// Media player for playback of calling/ringing/message sounds
private MediaPlayerWrapper mediaPlayerWrapper;
// 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 != null) {
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);
}
}
else {
RCLogger.e(TAG, "onProximitySensorChangedState called on null proximitySensor -check mem management");
}
}
}
/**
* Construction
*/
public 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(HashMap<String, Object> parameters)
{
RCLogger.d(TAG, "init");
if (initialized) {
return;
}
populateAudioResourceIds(parameters);
audioManager = ((AudioManager) apprtcContext.getSystemService(
Context.AUDIO_SERVICE));
mediaPlayerWrapper = new MediaPlayerWrapper(apprtcContext);
initialized = true;
}
public void close()
{
RCLogger.d(TAG, "close");
if (!initialized) {
return;
}
mediaPlayerWrapper.close();
initialized = false;
}
public void startCallMedia()
{
RCLogger.d(TAG, "startCall");
if (callAudioInitialized) {
return;
}
// 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(apprtcContext, 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();
}
});
// Store current audio state so we can restore it when close() is called.
savedAudioMode = audioManager.getMode();
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
savedIsMicrophoneMute = audioManager.isMicrophoneMute();
// Request audio focus before making any device switch.
audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN);
// 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
// best possible VoIP performance.
// TODO(henrika): we migh want to start with RINGTONE mode here instead.
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();
callAudioInitialized = true;
}
public void requestFocus()
{
}
public void abandonFocus()
{
}
public void endCallMedia()
{
RCLogger.d(TAG, "endCallMedia");
if (!callAudioInitialized) {
return;
}
unregisterForWiredHeadsetIntentBroadcast();
// Restore previously stored audio states.
setSpeakerphoneOn(savedIsSpeakerPhoneOn);
setMicrophoneMute(savedIsMicrophoneMute);
audioManager.setMode(savedAudioMode);
audioManager.abandonAudioFocus(null);
if (proximitySensor != null) {
proximitySensor.stop();
proximitySensor = null;
}
callAudioInitialized = false;
}
/**
* Populate audio resource ids for various sounds like calling, ringing, etc. The logic here is that
* we have default resources in the SDK level, found at R.raw.*, and if the user wants to override
* them they need to update R.raw in the Application level.
*
* @param parameters Dictionary of all parameters passed to RCDevice from the App
* @return Resource ids per resource name
*/
public void populateAudioResourceIds(HashMap<String, Object> parameters)
{
resourceIds = new HashMap<String, Integer>();
if (parameters.containsKey(RCDevice.ParameterKeys.RESOURCE_SOUND_CALLING)) {
resourceIds.put(RCDevice.ParameterKeys.RESOURCE_SOUND_CALLING, (Integer)parameters.get(RCDevice.ParameterKeys.RESOURCE_SOUND_CALLING));
}
else {
resourceIds.put(RCDevice.ParameterKeys.RESOURCE_SOUND_CALLING, R.raw.calling_sample);
}
if (parameters.containsKey(RCDevice.ParameterKeys.RESOURCE_SOUND_RINGING)) {
resourceIds.put(RCDevice.ParameterKeys.RESOURCE_SOUND_RINGING, (Integer)parameters.get(RCDevice.ParameterKeys.RESOURCE_SOUND_RINGING));
}
else {
resourceIds.put(RCDevice.ParameterKeys.RESOURCE_SOUND_RINGING, R.raw.ringing_sample);
}
if (parameters.containsKey(RCDevice.ParameterKeys.RESOURCE_SOUND_DECLINED)) {
resourceIds.put(RCDevice.ParameterKeys.RESOURCE_SOUND_DECLINED, (Integer)parameters.get(RCDevice.ParameterKeys.RESOURCE_SOUND_DECLINED));
}
else {
resourceIds.put(RCDevice.ParameterKeys.RESOURCE_SOUND_DECLINED, R.raw.busy_tone_sample);
}
if (parameters.containsKey(RCDevice.ParameterKeys.RESOURCE_SOUND_MESSAGE)) {
resourceIds.put(RCDevice.ParameterKeys.RESOURCE_SOUND_MESSAGE, (Integer)parameters.get(RCDevice.ParameterKeys.RESOURCE_SOUND_MESSAGE));
}
else {
resourceIds.put(RCDevice.ParameterKeys.RESOURCE_SOUND_MESSAGE, R.raw.message_sample);
}
}
public int getResourceIdForKey(String key)
{
return resourceIds.get(key);
}
/**
* Changes selection of the currently active audio device.
*/
public void setAudioDevice(AudioDevice device)
{
RCLogger.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:
RCLogger.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");
RCLogger.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:
RCLogger.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();
if (wasOn == on) {
return;
}
audioManager.setSpeakerphoneOn(on);
}
/**
* Sets the microphone mute state.
*/
private void setMicrophoneMute(boolean on)
{
boolean wasMuted = audioManager.isMicrophoneMute();
if (wasMuted == on) {
return;
}
audioManager.setMicrophoneMute(on);
}
public void setMute(boolean on)
{
audioManager.setMicrophoneMute(on);
}
public boolean getMute()
{
return audioManager.isMicrophoneMute();
}
/**
* 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);
}
}
RCLogger.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()
{
RCLogger.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 {
RCLogger.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();
}
}
// MediaPlayer related methods
public void playCallingSound()
{
play(resourceIds.get(RCDevice.ParameterKeys.RESOURCE_SOUND_CALLING), true);
//mediaPlayerWrapper.play(resourceIds.get(RCDevice.ParameterKeys.RESOURCE_SOUND_CALLING), true);
}
public void playRingingSound()
{
play(resourceIds.get(RCDevice.ParameterKeys.RESOURCE_SOUND_RINGING), true);
//mediaPlayerWrapper.play(resourceIds.get(RCDevice.ParameterKeys.RESOURCE_SOUND_RINGING), true);
}
public void playDeclinedSound()
{
play(resourceIds.get(RCDevice.ParameterKeys.RESOURCE_SOUND_DECLINED), false);
//mediaPlayerWrapper.play(resourceIds.get(RCDevice.ParameterKeys.RESOURCE_SOUND_DECLINED), false);
}
public void playMessageSound()
{
play(resourceIds.get(RCDevice.ParameterKeys.RESOURCE_SOUND_MESSAGE), false);
//mediaPlayerWrapper.play(resourceIds.get(RCDevice.ParameterKeys.RESOURCE_SOUND_MESSAGE), false);
}
public void play(int resid, boolean loop)
{
mediaPlayerWrapper.play(resid, loop);
}
public void stop()
{
mediaPlayerWrapper.stop();
}
public void onAudioFocusChange(int focusChange)
{
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
// Pause playback
RCLogger.i(TAG, "onAudioFocusChange(): AUDIOFOCUS_LOSS_TRANSIENT");
}
else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// Resume playback
RCLogger.i(TAG, "onAudioFocusChange(): AUDIOFOCUS_GAIN");
}
else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
// Stop playback
//am.unregisterMediaButtonEventReceiver(RemoteControlReceiver);
//am.abandonAudioFocus(afChangeListener);
RCLogger.i(TAG, "onAudioFocusChange(): AUDIOFOCUS_GAIN");
}
}
}