Files
frameworks_base/voip/java/android/net/sip/SipAudioCall.java
Hung-ying Tyan 5bd3782f24 Check if VoIP API is supported in SipManager.
This is to make SipManager.isVoipSupported() effective.
Also add NPE check now that we may return null SipAudioCall when VOIP is not
supported.

Bug: 3251016

Change-Id: Icd551123499f55eef190743b90980922893c4a13
2010-12-21 11:51:03 +08:00

1067 lines
38 KiB
Java

/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.sip;
import android.content.Context;
import android.media.AudioManager;
import android.net.rtp.AudioCodec;
import android.net.rtp.AudioGroup;
import android.net.rtp.AudioStream;
import android.net.rtp.RtpStream;
import android.net.sip.SimpleSessionDescription.Media;
import android.net.wifi.WifiManager;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Handles an Internet audio call over SIP. You can instantiate this class with {@link SipManager},
* using {@link SipManager#makeAudioCall makeAudioCall()} and {@link SipManager#takeAudioCall
* takeAudioCall()}.
*
* <p class="note"><strong>Note:</strong> Using this class require the
* {@link android.Manifest.permission#INTERNET} and
* {@link android.Manifest.permission#USE_SIP} permissions.<br/><br/>In addition, {@link
* #startAudio} requires the
* {@link android.Manifest.permission#RECORD_AUDIO},
* {@link android.Manifest.permission#ACCESS_WIFI_STATE}, and
* {@link android.Manifest.permission#WAKE_LOCK} permissions; and {@link #setSpeakerMode
* setSpeakerMode()} requires the
* {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p>
*/
public class SipAudioCall {
private static final String TAG = SipAudioCall.class.getSimpleName();
private static final boolean RELEASE_SOCKET = true;
private static final boolean DONT_RELEASE_SOCKET = false;
private static final int SESSION_TIMEOUT = 5; // in seconds
/** Listener for events relating to a SIP call, such as when a call is being
* recieved ("on ringing") or a call is outgoing ("on calling").
* <p>Many of these events are also received by {@link SipSession.Listener}.</p>
*/
public static class Listener {
/**
* Called when the call object is ready to make another call.
* The default implementation calls {@link #onChanged}.
*
* @param call the call object that is ready to make another call
*/
public void onReadyToCall(SipAudioCall call) {
onChanged(call);
}
/**
* Called when a request is sent out to initiate a new call.
* The default implementation calls {@link #onChanged}.
*
* @param call the call object that carries out the audio call
*/
public void onCalling(SipAudioCall call) {
onChanged(call);
}
/**
* Called when a new call comes in.
* The default implementation calls {@link #onChanged}.
*
* @param call the call object that carries out the audio call
* @param caller the SIP profile of the caller
*/
public void onRinging(SipAudioCall call, SipProfile caller) {
onChanged(call);
}
/**
* Called when a RINGING response is received for the INVITE request
* sent. The default implementation calls {@link #onChanged}.
*
* @param call the call object that carries out the audio call
*/
public void onRingingBack(SipAudioCall call) {
onChanged(call);
}
/**
* Called when the session is established.
* The default implementation calls {@link #onChanged}.
*
* @param call the call object that carries out the audio call
*/
public void onCallEstablished(SipAudioCall call) {
onChanged(call);
}
/**
* Called when the session is terminated.
* The default implementation calls {@link #onChanged}.
*
* @param call the call object that carries out the audio call
*/
public void onCallEnded(SipAudioCall call) {
onChanged(call);
}
/**
* Called when the peer is busy during session initialization.
* The default implementation calls {@link #onChanged}.
*
* @param call the call object that carries out the audio call
*/
public void onCallBusy(SipAudioCall call) {
onChanged(call);
}
/**
* Called when the call is on hold.
* The default implementation calls {@link #onChanged}.
*
* @param call the call object that carries out the audio call
*/
public void onCallHeld(SipAudioCall call) {
onChanged(call);
}
/**
* Called when an error occurs. The default implementation is no op.
*
* @param call the call object that carries out the audio call
* @param errorCode error code of this error
* @param errorMessage error message
* @see SipErrorCode
*/
public void onError(SipAudioCall call, int errorCode,
String errorMessage) {
// no-op
}
/**
* Called when an event occurs and the corresponding callback is not
* overridden. The default implementation is no op. Error events are
* not re-directed to this callback and are handled in {@link #onError}.
*/
public void onChanged(SipAudioCall call) {
// no-op
}
}
private Context mContext;
private SipProfile mLocalProfile;
private SipAudioCall.Listener mListener;
private SipSession mSipSession;
private long mSessionId = System.currentTimeMillis();
private String mPeerSd;
private AudioStream mAudioStream;
private AudioGroup mAudioGroup;
private boolean mInCall = false;
private boolean mMuted = false;
private boolean mHold = false;
private SipProfile mPendingCallRequest;
private WifiManager mWm;
private WifiManager.WifiLock mWifiHighPerfLock;
private int mErrorCode = SipErrorCode.NO_ERROR;
private String mErrorMessage;
/**
* Creates a call object with the local SIP profile.
* @param context the context for accessing system services such as
* ringtone, audio, WIFI etc
*/
public SipAudioCall(Context context, SipProfile localProfile) {
mContext = context;
mLocalProfile = localProfile;
mWm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
}
/**
* Sets the listener to listen to the audio call events. The method calls
* {@link #setListener setListener(listener, false)}.
*
* @param listener to listen to the audio call events of this object
* @see #setListener(Listener, boolean)
*/
public void setListener(SipAudioCall.Listener listener) {
setListener(listener, false);
}
/**
* Sets the listener to listen to the audio call events. A
* {@link SipAudioCall} can only hold one listener at a time. Subsequent
* calls to this method override the previous listener.
*
* @param listener to listen to the audio call events of this object
* @param callbackImmediately set to true if the caller wants to be called
* back immediately on the current state
*/
public void setListener(SipAudioCall.Listener listener,
boolean callbackImmediately) {
mListener = listener;
try {
if ((listener == null) || !callbackImmediately) {
// do nothing
} else if (mErrorCode != SipErrorCode.NO_ERROR) {
listener.onError(this, mErrorCode, mErrorMessage);
} else if (mInCall) {
if (mHold) {
listener.onCallHeld(this);
} else {
listener.onCallEstablished(this);
}
} else {
int state = getState();
switch (state) {
case SipSession.State.READY_TO_CALL:
listener.onReadyToCall(this);
break;
case SipSession.State.INCOMING_CALL:
listener.onRinging(this, getPeerProfile());
break;
case SipSession.State.OUTGOING_CALL:
listener.onCalling(this);
break;
case SipSession.State.OUTGOING_CALL_RING_BACK:
listener.onRingingBack(this);
break;
}
}
} catch (Throwable t) {
Log.e(TAG, "setListener()", t);
}
}
/**
* Checks if the call is established.
*
* @return true if the call is established
*/
public boolean isInCall() {
synchronized (this) {
return mInCall;
}
}
/**
* Checks if the call is on hold.
*
* @return true if the call is on hold
*/
public boolean isOnHold() {
synchronized (this) {
return mHold;
}
}
/**
* Closes this object. This object is not usable after being closed.
*/
public void close() {
close(true);
}
private synchronized void close(boolean closeRtp) {
if (closeRtp) stopCall(RELEASE_SOCKET);
mInCall = false;
mHold = false;
mSessionId = System.currentTimeMillis();
mErrorCode = SipErrorCode.NO_ERROR;
mErrorMessage = null;
if (mSipSession != null) {
mSipSession.setListener(null);
mSipSession = null;
}
}
/**
* Gets the local SIP profile.
*
* @return the local SIP profile
*/
public SipProfile getLocalProfile() {
synchronized (this) {
return mLocalProfile;
}
}
/**
* Gets the peer's SIP profile.
*
* @return the peer's SIP profile
*/
public SipProfile getPeerProfile() {
synchronized (this) {
return (mSipSession == null) ? null : mSipSession.getPeerProfile();
}
}
/**
* Gets the state of the {@link SipSession} that carries this call.
* The value returned must be one of the states in {@link SipSession.State}.
*
* @return the session state
*/
public int getState() {
synchronized (this) {
if (mSipSession == null) return SipSession.State.READY_TO_CALL;
return mSipSession.getState();
}
}
/**
* Gets the {@link SipSession} that carries this call.
*
* @return the session object that carries this call
* @hide
*/
public SipSession getSipSession() {
synchronized (this) {
return mSipSession;
}
}
private SipSession.Listener createListener() {
return new SipSession.Listener() {
@Override
public void onCalling(SipSession session) {
Log.d(TAG, "calling... " + session);
Listener listener = mListener;
if (listener != null) {
try {
listener.onCalling(SipAudioCall.this);
} catch (Throwable t) {
Log.i(TAG, "onCalling(): " + t);
}
}
}
@Override
public void onRingingBack(SipSession session) {
Log.d(TAG, "sip call ringing back: " + session);
Listener listener = mListener;
if (listener != null) {
try {
listener.onRingingBack(SipAudioCall.this);
} catch (Throwable t) {
Log.i(TAG, "onRingingBack(): " + t);
}
}
}
@Override
public void onRinging(SipSession session,
SipProfile peerProfile, String sessionDescription) {
synchronized (SipAudioCall.this) {
if ((mSipSession == null) || !mInCall
|| !session.getCallId().equals(
mSipSession.getCallId())) {
// should not happen
session.endCall();
return;
}
// session changing request
try {
String answer = createAnswer(sessionDescription).encode();
mSipSession.answerCall(answer, SESSION_TIMEOUT);
} catch (Throwable e) {
Log.e(TAG, "onRinging()", e);
session.endCall();
}
}
}
@Override
public void onCallEstablished(SipSession session,
String sessionDescription) {
mPeerSd = sessionDescription;
Log.v(TAG, "onCallEstablished()" + mPeerSd);
Listener listener = mListener;
if (listener != null) {
try {
if (mHold) {
listener.onCallHeld(SipAudioCall.this);
} else {
listener.onCallEstablished(SipAudioCall.this);
}
} catch (Throwable t) {
Log.i(TAG, "onCallEstablished(): " + t);
}
}
}
@Override
public void onCallEnded(SipSession session) {
Log.d(TAG, "sip call ended: " + session);
Listener listener = mListener;
if (listener != null) {
try {
listener.onCallEnded(SipAudioCall.this);
} catch (Throwable t) {
Log.i(TAG, "onCallEnded(): " + t);
}
}
close();
}
@Override
public void onCallBusy(SipSession session) {
Log.d(TAG, "sip call busy: " + session);
Listener listener = mListener;
if (listener != null) {
try {
listener.onCallBusy(SipAudioCall.this);
} catch (Throwable t) {
Log.i(TAG, "onCallBusy(): " + t);
}
}
close(false);
}
@Override
public void onCallChangeFailed(SipSession session, int errorCode,
String message) {
Log.d(TAG, "sip call change failed: " + message);
mErrorCode = errorCode;
mErrorMessage = message;
Listener listener = mListener;
if (listener != null) {
try {
listener.onError(SipAudioCall.this, mErrorCode,
message);
} catch (Throwable t) {
Log.i(TAG, "onCallBusy(): " + t);
}
}
}
@Override
public void onError(SipSession session, int errorCode,
String message) {
SipAudioCall.this.onError(errorCode, message);
}
@Override
public void onRegistering(SipSession session) {
// irrelevant
}
@Override
public void onRegistrationTimeout(SipSession session) {
// irrelevant
}
@Override
public void onRegistrationFailed(SipSession session, int errorCode,
String message) {
// irrelevant
}
@Override
public void onRegistrationDone(SipSession session, int duration) {
// irrelevant
}
};
}
private void onError(int errorCode, String message) {
Log.d(TAG, "sip session error: "
+ SipErrorCode.toString(errorCode) + ": " + message);
mErrorCode = errorCode;
mErrorMessage = message;
Listener listener = mListener;
if (listener != null) {
try {
listener.onError(this, errorCode, message);
} catch (Throwable t) {
Log.i(TAG, "onError(): " + t);
}
}
synchronized (this) {
if ((errorCode == SipErrorCode.DATA_CONNECTION_LOST)
|| !isInCall()) {
close(true);
}
}
}
/**
* Attaches an incoming call to this call object.
*
* @param session the session that receives the incoming call
* @param sessionDescription the session description of the incoming call
* @throws SipException if the SIP service fails to attach this object to
* the session or VOIP API is not supported by the device
* @see SipManager#isVoipSupported
*/
public void attachCall(SipSession session, String sessionDescription)
throws SipException {
if (!SipManager.isVoipSupported(mContext)) {
throw new SipException("VOIP API is not supported");
}
synchronized (this) {
mSipSession = session;
mPeerSd = sessionDescription;
Log.v(TAG, "attachCall()" + mPeerSd);
try {
session.setListener(createListener());
} catch (Throwable e) {
Log.e(TAG, "attachCall()", e);
throwSipException(e);
}
}
}
/**
* Initiates an audio call to the specified profile. The attempt will be
* timed out if the call is not established within {@code timeout} seconds
* and {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
* will be called.
*
* @param peerProfile the SIP profile to make the call to
* @param sipSession the {@link SipSession} for carrying out the call
* @param timeout the timeout value in seconds. Default value (defined by
* SIP protocol) is used if {@code timeout} is zero or negative.
* @see Listener#onError
* @throws SipException if the SIP service fails to create a session for the
* call or VOIP API is not supported by the device
* @see SipManager#isVoipSupported
*/
public void makeCall(SipProfile peerProfile, SipSession sipSession,
int timeout) throws SipException {
if (!SipManager.isVoipSupported(mContext)) {
throw new SipException("VOIP API is not supported");
}
synchronized (this) {
mSipSession = sipSession;
try {
mAudioStream = new AudioStream(InetAddress.getByName(
getLocalIp()));
sipSession.setListener(createListener());
sipSession.makeCall(peerProfile, createOffer().encode(),
timeout);
} catch (IOException e) {
throw new SipException("makeCall()", e);
}
}
}
/**
* Ends a call.
* @throws SipException if the SIP service fails to end the call
*/
public void endCall() throws SipException {
synchronized (this) {
stopCall(RELEASE_SOCKET);
mInCall = false;
// perform the above local ops first and then network op
if (mSipSession != null) mSipSession.endCall();
}
}
/**
* Puts a call on hold. When succeeds, {@link Listener#onCallHeld} is
* called. The attempt will be timed out if the call is not established
* within {@code timeout} seconds and
* {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
* will be called.
*
* @param timeout the timeout value in seconds. Default value (defined by
* SIP protocol) is used if {@code timeout} is zero or negative.
* @see Listener#onError
* @throws SipException if the SIP service fails to hold the call
*/
public void holdCall(int timeout) throws SipException {
synchronized (this) {
if (mHold) return;
if (mSipSession == null) {
throw new SipException("Not in a call to hold call");
}
mSipSession.changeCall(createHoldOffer().encode(), timeout);
mHold = true;
setAudioGroupMode();
}
}
/**
* Answers a call. The attempt will be timed out if the call is not
* established within {@code timeout} seconds and
* {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
* will be called.
*
* @param timeout the timeout value in seconds. Default value (defined by
* SIP protocol) is used if {@code timeout} is zero or negative.
* @see Listener#onError
* @throws SipException if the SIP service fails to answer the call
*/
public void answerCall(int timeout) throws SipException {
synchronized (this) {
if (mSipSession == null) {
throw new SipException("No call to answer");
}
try {
mAudioStream = new AudioStream(InetAddress.getByName(
getLocalIp()));
mSipSession.answerCall(createAnswer(mPeerSd).encode(), timeout);
} catch (IOException e) {
throw new SipException("answerCall()", e);
}
}
}
/**
* Continues a call that's on hold. When succeeds,
* {@link Listener#onCallEstablished} is called. The attempt will be timed
* out if the call is not established within {@code timeout} seconds and
* {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
* will be called.
*
* @param timeout the timeout value in seconds. Default value (defined by
* SIP protocol) is used if {@code timeout} is zero or negative.
* @see Listener#onError
* @throws SipException if the SIP service fails to unhold the call
*/
public void continueCall(int timeout) throws SipException {
synchronized (this) {
if (!mHold) return;
mSipSession.changeCall(createContinueOffer().encode(), timeout);
mHold = false;
setAudioGroupMode();
}
}
private SimpleSessionDescription createOffer() {
SimpleSessionDescription offer =
new SimpleSessionDescription(mSessionId, getLocalIp());
AudioCodec[] codecs = AudioCodec.getCodecs();
Media media = offer.newMedia(
"audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
for (AudioCodec codec : AudioCodec.getCodecs()) {
media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
}
media.setRtpPayload(127, "telephone-event/8000", "0-15");
return offer;
}
private SimpleSessionDescription createAnswer(String offerSd) {
SimpleSessionDescription offer =
new SimpleSessionDescription(offerSd);
SimpleSessionDescription answer =
new SimpleSessionDescription(mSessionId, getLocalIp());
AudioCodec codec = null;
for (Media media : offer.getMedia()) {
if ((codec == null) && (media.getPort() > 0)
&& "audio".equals(media.getType())
&& "RTP/AVP".equals(media.getProtocol())) {
// Find the first audio codec we supported.
for (int type : media.getRtpPayloadTypes()) {
codec = AudioCodec.getCodec(type, media.getRtpmap(type),
media.getFmtp(type));
if (codec != null) {
break;
}
}
if (codec != null) {
Media reply = answer.newMedia(
"audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
reply.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
// Check if DTMF is supported in the same media.
for (int type : media.getRtpPayloadTypes()) {
String rtpmap = media.getRtpmap(type);
if ((type != codec.type) && (rtpmap != null)
&& rtpmap.startsWith("telephone-event")) {
reply.setRtpPayload(
type, rtpmap, media.getFmtp(type));
}
}
// Handle recvonly and sendonly.
if (media.getAttribute("recvonly") != null) {
answer.setAttribute("sendonly", "");
} else if(media.getAttribute("sendonly") != null) {
answer.setAttribute("recvonly", "");
} else if(offer.getAttribute("recvonly") != null) {
answer.setAttribute("sendonly", "");
} else if(offer.getAttribute("sendonly") != null) {
answer.setAttribute("recvonly", "");
}
continue;
}
}
// Reject the media.
Media reply = answer.newMedia(
media.getType(), 0, 1, media.getProtocol());
for (String format : media.getFormats()) {
reply.setFormat(format, null);
}
}
if (codec == null) {
throw new IllegalStateException("Reject SDP: no suitable codecs");
}
return answer;
}
private SimpleSessionDescription createHoldOffer() {
SimpleSessionDescription offer = createContinueOffer();
offer.setAttribute("sendonly", "");
return offer;
}
private SimpleSessionDescription createContinueOffer() {
SimpleSessionDescription offer =
new SimpleSessionDescription(mSessionId, getLocalIp());
Media media = offer.newMedia(
"audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
AudioCodec codec = mAudioStream.getCodec();
media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
int dtmfType = mAudioStream.getDtmfType();
if (dtmfType != -1) {
media.setRtpPayload(dtmfType, "telephone-event/8000", "0-15");
}
return offer;
}
private void grabWifiHighPerfLock() {
if (mWifiHighPerfLock == null) {
Log.v(TAG, "acquire wifi high perf lock");
mWifiHighPerfLock = ((WifiManager)
mContext.getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TAG);
mWifiHighPerfLock.acquire();
}
}
private void releaseWifiHighPerfLock() {
if (mWifiHighPerfLock != null) {
Log.v(TAG, "release wifi high perf lock");
mWifiHighPerfLock.release();
mWifiHighPerfLock = null;
}
}
private boolean isWifiOn() {
return (mWm.getConnectionInfo().getBSSID() == null) ? false : true;
}
/** Toggles mute. */
public void toggleMute() {
synchronized (this) {
mMuted = !mMuted;
setAudioGroupMode();
}
}
/**
* Checks if the call is muted.
*
* @return true if the call is muted
*/
public boolean isMuted() {
synchronized (this) {
return mMuted;
}
}
/**
* Puts the device to speaker mode.
* <p class="note"><strong>Note:</strong> Requires the
* {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p>
*
* @param speakerMode set true to enable speaker mode; false to disable
*/
public void setSpeakerMode(boolean speakerMode) {
synchronized (this) {
((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
.setSpeakerphoneOn(speakerMode);
setAudioGroupMode();
}
}
private boolean isSpeakerOn() {
return ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
.isSpeakerphoneOn();
}
/**
* Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>,
* event 0--9 maps to decimal
* value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event
* flash to 16. Currently, event flash is not supported.
*
* @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid
* inputs.
*/
public void sendDtmf(int code) {
sendDtmf(code, null);
}
/**
* Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>,
* event 0--9 maps to decimal
* value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event
* flash to 16. Currently, event flash is not supported.
*
* @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid
* inputs.
* @param result the result message to send when done
*/
public void sendDtmf(int code, Message result) {
synchronized (this) {
AudioGroup audioGroup = getAudioGroup();
if ((audioGroup != null) && (mSipSession != null)
&& (SipSession.State.IN_CALL == getState())) {
Log.v(TAG, "send DTMF: " + code);
audioGroup.sendDtmf(code);
}
if (result != null) result.sendToTarget();
}
}
/**
* Gets the {@link AudioStream} object used in this call. The object
* represents the RTP stream that carries the audio data to and from the
* peer. The object may not be created before the call is established. And
* it is undefined after the call ends or the {@link #close} method is
* called.
*
* @return the {@link AudioStream} object or null if the RTP stream has not
* yet been set up
* @hide
*/
public AudioStream getAudioStream() {
synchronized (this) {
return mAudioStream;
}
}
/**
* Gets the {@link AudioGroup} object which the {@link AudioStream} object
* joins. The group object may not exist before the call is established.
* Also, the {@code AudioStream} may change its group during a call (e.g.,
* after the call is held/un-held). Finally, the {@code AudioGroup} object
* returned by this method is undefined after the call ends or the
* {@link #close} method is called. If a group object is set by
* {@link #setAudioGroup(AudioGroup)}, then this method returns that object.
*
* @return the {@link AudioGroup} object or null if the RTP stream has not
* yet been set up
* @see #getAudioStream
* @hide
*/
public AudioGroup getAudioGroup() {
synchronized (this) {
if (mAudioGroup != null) return mAudioGroup;
return ((mAudioStream == null) ? null : mAudioStream.getGroup());
}
}
/**
* Sets the {@link AudioGroup} object which the {@link AudioStream} object
* joins. If {@code audioGroup} is null, then the {@code AudioGroup} object
* will be dynamically created when needed. Note that the mode of the
* {@code AudioGroup} is not changed according to the audio settings (i.e.,
* hold, mute, speaker phone) of this object. This is mainly used to merge
* multiple {@code SipAudioCall} objects to form a conference call. The
* settings of the first object (that merges others) override others'.
*
* @see #getAudioStream
* @hide
*/
public void setAudioGroup(AudioGroup group) {
synchronized (this) {
if ((mAudioStream != null) && (mAudioStream.getGroup() != null)) {
mAudioStream.join(group);
}
mAudioGroup = group;
}
}
/**
* Starts the audio for the established call. This method should be called
* after {@link Listener#onCallEstablished} is called.
* <p class="note"><strong>Note:</strong> Requires the
* {@link android.Manifest.permission#RECORD_AUDIO},
* {@link android.Manifest.permission#ACCESS_WIFI_STATE} and
* {@link android.Manifest.permission#WAKE_LOCK} permissions.</p>
*/
public void startAudio() {
try {
startAudioInternal();
} catch (UnknownHostException e) {
onError(SipErrorCode.PEER_NOT_REACHABLE, e.getMessage());
} catch (Throwable e) {
onError(SipErrorCode.CLIENT_ERROR, e.getMessage());
}
}
private synchronized void startAudioInternal() throws UnknownHostException {
if (mPeerSd == null) {
Log.v(TAG, "startAudioInternal() mPeerSd = null");
throw new IllegalStateException("mPeerSd = null");
}
stopCall(DONT_RELEASE_SOCKET);
mInCall = true;
// Run exact the same logic in createAnswer() to setup mAudioStream.
SimpleSessionDescription offer =
new SimpleSessionDescription(mPeerSd);
AudioStream stream = mAudioStream;
AudioCodec codec = null;
for (Media media : offer.getMedia()) {
if ((codec == null) && (media.getPort() > 0)
&& "audio".equals(media.getType())
&& "RTP/AVP".equals(media.getProtocol())) {
// Find the first audio codec we supported.
for (int type : media.getRtpPayloadTypes()) {
codec = AudioCodec.getCodec(
type, media.getRtpmap(type), media.getFmtp(type));
if (codec != null) {
break;
}
}
if (codec != null) {
// Associate with the remote host.
String address = media.getAddress();
if (address == null) {
address = offer.getAddress();
}
stream.associate(InetAddress.getByName(address),
media.getPort());
stream.setDtmfType(-1);
stream.setCodec(codec);
// Check if DTMF is supported in the same media.
for (int type : media.getRtpPayloadTypes()) {
String rtpmap = media.getRtpmap(type);
if ((type != codec.type) && (rtpmap != null)
&& rtpmap.startsWith("telephone-event")) {
stream.setDtmfType(type);
}
}
// Handle recvonly and sendonly.
if (mHold) {
stream.setMode(RtpStream.MODE_NORMAL);
} else if (media.getAttribute("recvonly") != null) {
stream.setMode(RtpStream.MODE_SEND_ONLY);
} else if(media.getAttribute("sendonly") != null) {
stream.setMode(RtpStream.MODE_RECEIVE_ONLY);
} else if(offer.getAttribute("recvonly") != null) {
stream.setMode(RtpStream.MODE_SEND_ONLY);
} else if(offer.getAttribute("sendonly") != null) {
stream.setMode(RtpStream.MODE_RECEIVE_ONLY);
} else {
stream.setMode(RtpStream.MODE_NORMAL);
}
break;
}
}
}
if (codec == null) {
throw new IllegalStateException("Reject SDP: no suitable codecs");
}
if (isWifiOn()) grabWifiHighPerfLock();
if (!mHold) {
/* The recorder volume will be very low if the device is in
* IN_CALL mode. Therefore, we have to set the mode to NORMAL
* in order to have the normal microphone level.
*/
((AudioManager) mContext.getSystemService
(Context.AUDIO_SERVICE))
.setMode(AudioManager.MODE_NORMAL);
}
// AudioGroup logic:
AudioGroup audioGroup = getAudioGroup();
if (mHold) {
// don't create an AudioGroup here; doing so will fail if
// there's another AudioGroup out there that's active
} else {
if (audioGroup == null) audioGroup = new AudioGroup();
stream.join(audioGroup);
}
setAudioGroupMode();
}
// set audio group mode based on current audio configuration
private void setAudioGroupMode() {
AudioGroup audioGroup = getAudioGroup();
if (audioGroup != null) {
if (mHold) {
audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
} else if (mMuted) {
audioGroup.setMode(AudioGroup.MODE_MUTED);
} else if (isSpeakerOn()) {
audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION);
} else {
audioGroup.setMode(AudioGroup.MODE_NORMAL);
}
}
}
private void stopCall(boolean releaseSocket) {
Log.d(TAG, "stop audiocall");
releaseWifiHighPerfLock();
if (mAudioStream != null) {
mAudioStream.join(null);
if (releaseSocket) {
mAudioStream.release();
mAudioStream = null;
}
}
}
private String getLocalIp() {
return mSipSession.getLocalIp();
}
private void throwSipException(Throwable throwable) throws SipException {
if (throwable instanceof SipException) {
throw (SipException) throwable;
} else {
throw new SipException("", throwable);
}
}
private SipProfile getPeerProfile(SipSession session) {
return session.getPeerProfile();
}
}