blob: 0acd5a640d2f95e8179335fca72692fab37c7802 [file] [log] [blame]
/*
* Copyright (C) 2009 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.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.net.DatagramSocket;
import java.text.ParseException;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TooManyListenersException;
import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.DialogTerminatedEvent;
import javax.sip.IOExceptionEvent;
import javax.sip.InvalidArgumentException;
import javax.sip.ListeningPoint;
import javax.sip.RequestEvent;
import javax.sip.ResponseEvent;
import javax.sip.ServerTransaction;
import javax.sip.SipException;
import javax.sip.SipFactory;
import javax.sip.SipListener;
import javax.sip.SipProvider;
import javax.sip.SipStack;
import javax.sip.TimeoutEvent;
import javax.sip.Transaction;
import javax.sip.TransactionTerminatedEvent;
import javax.sip.address.Address;
import javax.sip.address.SipURI;
import javax.sip.header.CSeqHeader;
import javax.sip.header.ExpiresHeader;
import javax.sip.header.FromHeader;
import javax.sip.message.Message;
import javax.sip.message.Request;
import javax.sip.message.Response;
/**
* Manages SipSession's for a SIP account.
*/
class SipSessionGroup implements SipListener {
private static final String TAG = SipSessionGroup.class.getSimpleName();
private static final int EXPIRY_TIME = 3600;
private static final EventObject REGISTER = new EventObject("Register");
private static final EventObject DEREGISTER = new EventObject("Deregister");
private static final EventObject END_CALL = new EventObject("End call");
private static final EventObject HOLD_CALL = new EventObject("Hold call");
private static final EventObject CONTINUE_CALL
= new EventObject("Continue call");
private static final String STACK_NAME = "A SIP STACK";
private SipStack mSipStack;
private SipHelper mSipHelper;
private SipProfile mLocalProfile;
// default session that processes all the un-attended messages received
// from the UDP port
private SipSessionImpl mDefaultSession;
// call-id-to-SipSession map
private Map<String, SipSessionImpl> mSessionMap =
new HashMap<String, SipSessionImpl>();
SipSessionGroup(String localIp, SipProfile myself,
SipSessionListener listener) throws SipException {
SipFactory sipFactory = SipFactory.getInstance();
Properties properties = new Properties();
properties.setProperty("javax.sip.STACK_NAME", STACK_NAME);
String outboundProxy = myself.getOutboundProxy();
if (!TextUtils.isEmpty(outboundProxy)) {
properties.setProperty("javax.sip.OUTBOUND_PROXY", outboundProxy);
}
SipStack stack = mSipStack = sipFactory.createSipStack(properties);
stack.start();
SipProvider provider = stack.createSipProvider(
stack.createListeningPoint(localIp, allocateLocalPort(),
ListeningPoint.UDP));
try {
provider.addSipListener(this);
} catch (TooManyListenersException e) {
// must never happen
throw new SipException("SipSessionGroup constructor", e);
}
mSipHelper = new SipHelper(stack, provider);
mLocalProfile = myself;
mDefaultSession = new SipSessionImpl(listener);
}
public synchronized void close() {
if (mSipStack != null) {
mSipStack.stop();
mSipStack = null;
}
}
SipSession getDefaultSession() {
return mDefaultSession;
}
private static int allocateLocalPort() throws SipException {
try {
DatagramSocket s = new DatagramSocket();
int localPort = s.getLocalPort();
s.close();
return localPort;
} catch (IOException e) {
throw new SipException("allocateLocalPort()", e);
}
}
private synchronized SipSessionImpl getSipSession(EventObject event) {
String[] keys = mSipHelper.getPossibleSessionKeys(event);
Log.v(TAG, " possible keys " + keys.length + " for evt: " + event);
for (String key : keys) Log.v(TAG, " '" + key + "'");
Log.v(TAG, " active sessions:");
for (String k : mSessionMap.keySet()) {
Log.v(TAG, " ----- '" + k + "': " + mSessionMap.get(k));
}
for (String uri : mSipHelper.getPossibleSessionKeys(event)) {
SipSessionImpl callSession = mSessionMap.get(uri);
// return first match
if (callSession != null) return callSession;
}
return mDefaultSession;
}
private synchronized void addSipSession(SipSessionImpl newSession) {
String key = mSipHelper.getSessionKey(newSession);
Log.v(TAG, " +++++ add a session with key: '" + key + "'");
mSessionMap.put(key, newSession);
for (String k : mSessionMap.keySet()) {
Log.v(TAG, " ..... " + k + ": " + mSessionMap.get(k));
}
}
private synchronized void removeSipSession(SipSessionImpl session) {
String key = mSipHelper.getSessionKey(session);
SipSessionImpl s = mSessionMap.remove(key);
// sanity check
if ((s != null) && (s != session)) {
Log.w(TAG, "session " + session + " is not associated with key '"
+ key + "'");
mSessionMap.put(key, s);
} else {
Log.v(TAG, " remove session " + session + " with key '" + key
+ "'");
}
for (String k : mSessionMap.keySet()) {
Log.v(TAG, " ..... " + k + ": " + mSessionMap.get(k));
}
}
public void processRequest(RequestEvent event) {
process(event);
}
public void processResponse(ResponseEvent event) {
process(event);
}
public void processIOException(IOExceptionEvent event) {
// TODO: find proper listener to pass the exception
process(event);
}
public void processTimeout(TimeoutEvent event) {
// TODO: find proper listener to pass the exception
process(event);
}
public void processTransactionTerminated(TransactionTerminatedEvent event) {
// TODO: clean up if session is in in-transaction states
//process(event);
}
public void processDialogTerminated(DialogTerminatedEvent event) {
process(event);
}
private void process(EventObject event) {
SipSessionImpl session = getSipSession(event);
if (session == null) {
Log.w(TAG, "event not processed: " + event);
return;
}
try {
session.process(event);
} catch (Throwable e) {
Log.e(TAG, "event process error: " + event, e);
session.mListener.onError(session, e);
}
}
private class SipSessionImpl implements SipSession {
private SipProfile mPeerProfile;
private SipSessionListener mListener;
private SipSessionState mState = SipSessionState.READY_FOR_CALL;
private RequestEvent mInviteReceived;
private Dialog mDialog;
private ServerTransaction mServerTransaction;
private ClientTransaction mClientTransaction;
private byte[] mPeerSessionDescription;
SipSessionImpl(SipSessionListener listener) {
mListener = listener;
}
private void reset() {
mPeerProfile = null;
mState = SipSessionState.READY_FOR_CALL;
mInviteReceived = null;
mDialog = null;
mServerTransaction = null;
mClientTransaction = null;
mPeerSessionDescription = null;
}
public SipProfile getLocalProfile() {
return mLocalProfile;
}
public synchronized SipProfile getPeerProfile() {
return mPeerProfile;
}
public synchronized Dialog getDialog() {
return mDialog;
}
public synchronized SipSessionState getState() {
return mState;
}
public void makeCall(SipProfile peerProfile,
SessionDescription sessionDescription) throws SipException {
process(new MakeCallCommand(peerProfile, sessionDescription));
}
public void answerCall(SessionDescription sessionDescription)
throws SipException {
process(new MakeCallCommand(mPeerProfile, sessionDescription));
}
public void endCall() throws SipException {
process(END_CALL);
}
// http://www.tech-invite.com/Ti-sip-service-1.html#fig5
public void changeCall(SessionDescription sessionDescription)
throws SipException {
process(new MakeCallCommand(mPeerProfile, sessionDescription));
}
public void register() throws SipException {
process(REGISTER);
}
public void deRegister() throws SipException {
process(DEREGISTER);
}
private void scheduleNextRegistration(
ExpiresHeader expiresHeader) {
int expires = EXPIRY_TIME;
if (expiresHeader != null) expires = expiresHeader.getExpires();
Log.v(TAG, "Refresh registration " + expires + "s later.");
new Timer().schedule(new TimerTask() {
public void run() {
try {
process(REGISTER);
} catch (SipException e) {
Log.e(TAG, "", e);
}
}}, expires * 1000L);
}
private String generateTag() {
// TODO: based on myself's profile
return String.valueOf((long) (Math.random() * 1000000L));
}
private String log(EventObject evt) {
if (evt instanceof RequestEvent) {
return ((RequestEvent) evt).getRequest().toString();
} else if (evt instanceof ResponseEvent) {
return ((ResponseEvent) evt).getResponse().toString();
} else {
return evt.toString();
}
}
public String toString() {
try {
String s = super.toString();
return s.substring(s.indexOf("@")) + ":" + mState;
} catch (Throwable e) {
return super.toString();
}
}
synchronized void process(EventObject evt) throws SipException {
Log.v(TAG, " ~~~~~ " + this + ": " + mState + ": processing "
+ log(evt));
boolean processed;
switch (mState) {
case REGISTERING:
case DEREGISTERING:
processed = registeringToReady(evt);
break;
case READY_FOR_CALL:
processed = readyForCall(evt);
break;
case INCOMING_CALL:
processed = incomingCall(evt);
break;
case INCOMING_CALL_ANSWERING:
processed = incomingCallToInCall(evt);
break;
case OUTGOING_CALL:
case OUTGOING_CALL_RING_BACK:
processed = outgoingCall(evt);
break;
case OUTGOING_CALL_CANCELING:
processed = outgoingCallToReady(evt);
break;
case IN_CALL:
processed = inCall(evt);
break;
case IN_CALL_ANSWERING:
processed = inCallAnsweringToInCall(evt);
break;
case IN_CALL_CHANGING:
processed = inCallChanging(evt);
break;
case IN_CALL_CHANGING_CANCELING:
processed = inCallChangingToInCall(evt);
break;
default:
processed = false;
}
if (!processed && !processExceptions(evt)) {
Log.w(TAG, "event not processed: " + evt);
} else {
Log.v(TAG, " ~~~~~ new state: " + mState);
}
}
private boolean processExceptions(EventObject evt) throws SipException {
// process INVITE and CANCEL
if (isRequestEvent(Request.INVITE, evt)) {
mSipHelper.sendResponse((RequestEvent) evt, Response.BUSY_HERE);
return true;
} else if (isRequestEvent(Request.CANCEL, evt)) {
mSipHelper.sendResponse((RequestEvent) evt,
Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST);
return true;
} else if (evt instanceof TransactionTerminatedEvent) {
if (evt instanceof TimeoutEvent) {
processTimeout((TimeoutEvent) evt);
} else {
// TODO: any clean up?
}
return true;
} else if (evt instanceof DialogTerminatedEvent) {
processDialogTerminated((DialogTerminatedEvent) evt);
return true;
}
return false;
}
private void processDialogTerminated(DialogTerminatedEvent event) {
if (mDialog == event.getDialog()) {
endCall(isInCall());
} else {
Log.d(TAG, "not the current dialog; current=" + mDialog
+ ", terminated=" + event.getDialog());
}
}
private void processTimeout(TimeoutEvent event) {
Log.d(TAG, "processing Timeout..." + event);
Transaction current = event.isServerTransaction()
? mServerTransaction
: mClientTransaction;
Transaction target = event.isServerTransaction()
? event.getServerTransaction()
: event.getClientTransaction();
if (current != target) {
Log.d(TAG, "not the current transaction; current=" + current
+ ", timed out=" + target);
return;
}
switch (mState) {
case REGISTERING:
case INCOMING_CALL:
case INCOMING_CALL_ANSWERING:
case OUTGOING_CALL:
case OUTGOING_CALL_RING_BACK:
case OUTGOING_CALL_CANCELING:
case IN_CALL_CHANGING:
// rfc3261#section-14.1
// if re-invite gets timed out, terminate the dialog
endCallOnError((mState == SipSessionState.IN_CALL_CHANGING),
new SipException("timed out"));
break;
case IN_CALL_ANSWERING:
case IN_CALL_CHANGING_CANCELING:
mState = SipSessionState.IN_CALL;
mListener.onError(this, new SipException("timed out"));
break;
default:
// do nothing
break;
}
}
private boolean registeringToReady(EventObject evt)
throws SipException {
if (expectResponse(Request.REGISTER, evt)) {
ResponseEvent event = (ResponseEvent) evt;
Response response = event.getResponse();
int statusCode = response.getStatusCode();
switch (statusCode) {
case Response.OK:
if (mState == SipSessionState.REGISTERING) {
scheduleNextRegistration((ExpiresHeader)
((ResponseEvent) evt).getResponse().getHeader(
ExpiresHeader.NAME));
}
reset();
mListener.onRegistrationDone(this);
return true;
case Response.UNAUTHORIZED:
case Response.PROXY_AUTHENTICATION_REQUIRED:
mSipHelper.handleChallenge((ResponseEvent)evt, mLocalProfile);
return true;
default:
if (statusCode >= 500) {
reset();
mListener.onRegistrationFailed(this,
createCallbackException(response));
return true;
}
}
}
return false;
}
private boolean readyForCall(EventObject evt) throws SipException {
// expect MakeCallCommand, Invite
if (evt instanceof MakeCallCommand) {
MakeCallCommand cmd = (MakeCallCommand) evt;
mPeerProfile = cmd.getPeerProfile();
SessionDescription sessionDescription =
cmd.getSessionDescription();
mClientTransaction = mSipHelper.sendInvite(mLocalProfile,
mPeerProfile, sessionDescription, generateTag());
mDialog = mClientTransaction.getDialog();
mState = SipSessionState.OUTGOING_CALL;
mListener.onCalling(SipSessionImpl.this);
return true;
} else if (isRequestEvent(Request.INVITE, evt)) {
RequestEvent event = (RequestEvent) evt;
mServerTransaction = mSipHelper.sendRinging(event);
mDialog = mServerTransaction.getDialog();
mInviteReceived = event;
mPeerProfile = createPeerProfile(event.getRequest());
mState = SipSessionState.INCOMING_CALL;
mPeerSessionDescription = event.getRequest().getRawContent();
mListener.onRinging(SipSessionImpl.this, mPeerProfile,
mPeerSessionDescription);
return true;
} else if (REGISTER == evt) {
mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
generateTag(), EXPIRY_TIME);
mDialog = mClientTransaction.getDialog();
mState = SipSessionState.REGISTERING;
return true;
} else if (DEREGISTER == evt) {
mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
generateTag(), 0);
mState = SipSessionState.DEREGISTERING;
return true;
}
return false;
}
private boolean incomingCall(EventObject evt) throws SipException {
// expect MakeCallCommand(answering) , END_CALL cmd , Cancel
if (evt instanceof MakeCallCommand) {
// answer call
mSipHelper.sendInviteOk(mInviteReceived, mLocalProfile,
((MakeCallCommand) evt).getSessionDescription(),
generateTag(), mServerTransaction);
mState = SipSessionState.INCOMING_CALL_ANSWERING;
return true;
} else if (END_CALL == evt) {
mSipHelper.sendInviteBusyHere(mInviteReceived,
mServerTransaction);
endCall(false);
return true;
} else if (isRequestEvent(Request.CANCEL, evt)) {
RequestEvent event = (RequestEvent) evt;
mSipHelper.sendResponse(event, Response.OK);
mSipHelper.sendInviteRequestTerminated(
mInviteReceived.getRequest(), mServerTransaction);
endCall(false);
return true;
}
return false;
}
private boolean incomingCallToInCall(EventObject evt)
throws SipException {
// expect ACK, CANCEL request
if (isRequestEvent(Request.ACK, evt)) {
establishCall(false);
return true;
} else if (isRequestEvent(Request.CANCEL, evt)) {
RequestEvent event = (RequestEvent) evt;
// TODO: what to do here? what happens when racing between
// OK-to-invite from callee and Cancel from caller
return true;
}
return false;
}
private boolean outgoingCall(EventObject evt) throws SipException {
if (expectResponse(Request.INVITE, evt)) {
ResponseEvent event = (ResponseEvent) evt;
Response response = event.getResponse();
int statusCode = response.getStatusCode();
switch (statusCode) {
case Response.RINGING:
if (mState == SipSessionState.OUTGOING_CALL) {
mState = SipSessionState.OUTGOING_CALL_RING_BACK;
mListener.onRingingBack(this);
}
return true;
case Response.OK:
mSipHelper.sendInviteAck(event, mDialog);
mPeerSessionDescription = response.getRawContent();
establishCall(false);
return true;
case Response.PROXY_AUTHENTICATION_REQUIRED:
mClientTransaction = mSipHelper.handleChallenge(
(ResponseEvent)evt, mLocalProfile);
mDialog = mClientTransaction.getDialog();
return true;
default:
if (statusCode >= 400) {
// error: an ack is sent automatically by the stack
endCallOnError(false,
createCallbackException(response));
return true;
} else if (statusCode >= 300) {
// TODO: handle 3xx (redirect)
} else {
return true;
}
}
return false;
} else if (END_CALL == evt) {
// RFC says that UA should not send out cancel when no
// response comes back yet. We are cheating for not checking
// response.
mSipHelper.sendCancel(mClientTransaction);
mState = SipSessionState.OUTGOING_CALL_CANCELING;
return true;
}
return false;
}
private boolean outgoingCallToReady(EventObject evt)
throws SipException {
if (evt instanceof ResponseEvent) {
ResponseEvent event = (ResponseEvent) evt;
Response response = event.getResponse();
int statusCode = response.getStatusCode();
if (expectResponse(Request.CANCEL, evt)) {
if (statusCode == Response.OK) {
// do nothing; wait for REQUEST_TERMINATED
return true;
}
} else if (expectResponse(Request.INVITE, evt)) {
if (statusCode == Response.REQUEST_TERMINATED) {
endCall(false);
return true;
}
} else {
return false;
}
if (statusCode >= 400) {
endCallOnError(true, createCallbackException(response));
return true;
}
}
return false;
}
private boolean inCall(EventObject evt) throws SipException {
// expect END_CALL cmd, BYE request, hold call (MakeCallCommand)
// OK retransmission is handled in SipStack
if (END_CALL == evt) {
// rfc3261#section-15.1.1
mSipHelper.sendBye(mDialog);
endCall(true);
return true;
} else if (isRequestEvent(Request.INVITE, evt)) {
// got Re-INVITE
RequestEvent event = (RequestEvent) evt;
mSipHelper.sendReInviteOk(event, mLocalProfile);
mState = SipSessionState.IN_CALL_ANSWERING;
mPeerSessionDescription = event.getRequest().getRawContent();
mListener.onCallChanged(this, mPeerSessionDescription);
return true;
} else if (isRequestEvent(Request.BYE, evt)) {
mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
endCall(true);
return true;
} else if (evt instanceof MakeCallCommand) {
// to change call
mClientTransaction = mSipHelper.sendReinvite(mDialog,
((MakeCallCommand) evt).getSessionDescription());
mState = SipSessionState.IN_CALL_CHANGING;
return true;
}
return false;
}
private boolean inCallChanging(EventObject evt)
throws SipException {
if (expectResponse(Request.INVITE, evt)) {
ResponseEvent event = (ResponseEvent) evt;
Response response = event.getResponse();
int statusCode = response.getStatusCode();
switch (statusCode) {
case Response.OK:
mSipHelper.sendInviteAck(event, mDialog);
establishCall(true);
return true;
case Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST:
case Response.REQUEST_TIMEOUT:
// rfc3261#section-14.1: re-invite failed; terminate
// the dialog
endCallOnError(true, createCallbackException(response));
return true;
case Response.REQUEST_PENDING:
// TODO:
// rfc3261#section-14.1; re-schedule invite
return true;
default:
if (statusCode >= 400) {
// error: an ack is sent automatically by the stack
mState = SipSessionState.IN_CALL;
mListener.onError(this,
createCallbackException(response));
return true;
} else if (statusCode >= 300) {
// TODO: handle 3xx (redirect)
} else {
return true;
}
}
return false;
} else if (END_CALL == evt) {
mSipHelper.sendCancel(mClientTransaction);
mState = SipSessionState.IN_CALL_CHANGING_CANCELING;
return true;
}
return false;
}
private boolean inCallChangingToInCall(EventObject evt)
throws SipException {
if (expectResponse(Response.OK, Request.CANCEL, evt)) {
// do nothing; wait for REQUEST_TERMINATED
return true;
} else if (expectResponse(Response.OK, Request.INVITE, evt)) {
inCallChanging(evt); // abort Cancel
return true;
} else if (expectResponse(Response.REQUEST_TERMINATED,
Request.INVITE, evt)) {
establishCall(true);
return true;
}
return false;
}
private boolean inCallAnsweringToInCall(EventObject evt) {
// expect ACK
if (isRequestEvent(Request.ACK, evt)) {
establishCall(true);
return true;
}
return false;
}
private Exception createCallbackException(Response response) {
return new SipException(String.format("Response: %s (%d)",
response.getReasonPhrase(), response.getStatusCode()));
}
private void establishCall(boolean inCall) {
if (inCall) {
mState = SipSessionState.IN_CALL;
mListener.onCallEstablished(this, mPeerSessionDescription);
} else {
SipSessionImpl newSession = createInCallSipSession();
addSipSession(newSession);
reset();
newSession.establishCall(true);
}
}
private void endCall(boolean inCall) {
if (inCall) removeSipSession(this);
reset();
mListener.onCallEnded(this);
}
private void endCallOnError(
boolean terminating, Throwable throwable) {
if (terminating) removeSipSession(this);
reset();
mListener.onError(this, throwable);
}
private boolean isInCall() {
return (mState.compareTo(SipSessionState.IN_CALL) >= 0);
}
private SipSessionImpl createInCallSipSession() {
SipSessionImpl newSession = new SipSessionImpl(mListener);
newSession.mPeerProfile = mPeerProfile;
newSession.mState = SipSessionState.IN_CALL;
newSession.mInviteReceived = mInviteReceived;
newSession.mDialog = mDialog;
newSession.mPeerSessionDescription = mPeerSessionDescription;
return newSession;
}
}
/**
* @return true if the event is a request event matching the specified
* method; false otherwise
*/
private static boolean isRequestEvent(String method, EventObject event) {
try {
if (event instanceof RequestEvent) {
RequestEvent requestEvent = (RequestEvent) event;
return method.equals(requestEvent.getRequest().getMethod());
}
} catch (Throwable e) {
}
return false;
}
private static String getCseqMethod(Message message) {
return ((CSeqHeader) message.getHeader(CSeqHeader.NAME)).getMethod();
}
/**
* @return true if the event is a response event and the CSeqHeader method
* match the given arguments; false otherwise
*/
private static boolean expectResponse(
String expectedMethod, EventObject evt) {
if (evt instanceof ResponseEvent) {
ResponseEvent event = (ResponseEvent) evt;
Response response = event.getResponse();
return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
}
return false;
}
/**
* @return true if the event is a response event and the response code and
* CSeqHeader method match the given arguments; false otherwise
*/
private static boolean expectResponse(
int responseCode, String expectedMethod, EventObject evt) {
if (evt instanceof ResponseEvent) {
ResponseEvent event = (ResponseEvent) evt;
Response response = event.getResponse();
if (response.getStatusCode() == responseCode) {
return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
}
}
return false;
}
private static SipProfile createPeerProfile(Request request)
throws SipException {
try {
FromHeader fromHeader =
(FromHeader) request.getHeader(FromHeader.NAME);
Address address = fromHeader.getAddress();
SipURI uri = (SipURI) address.getURI();
return new SipProfile.Builder(uri.getUser(), uri.getHost())
.setPort(uri.getPort())
.setDisplayName(address.getDisplayName())
.build();
} catch (InvalidArgumentException e) {
throw new SipException("createPeerProfile()", e);
} catch (ParseException e) {
throw new SipException("createPeerProfile()", e);
}
}
private class MakeCallCommand extends EventObject {
private SessionDescription mSessionDescription;
MakeCallCommand(SipProfile peerProfile,
SessionDescription sessionDescription) {
super(peerProfile);
mSessionDescription = sessionDescription;
}
SipProfile getPeerProfile() {
return (SipProfile) getSource();
}
SessionDescription getSessionDescription() {
return mSessionDescription;
}
}
}