blob: e820f356bea348386e63cbe0d4454ef8c82b1014 [file] [log] [blame]
/*
* 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 com.android.server.sip;
import gov.nist.javax.sip.clientauthutils.AccountManager;
import gov.nist.javax.sip.clientauthutils.UserCredentials;
import gov.nist.javax.sip.header.ProxyAuthenticate;
import gov.nist.javax.sip.header.ReferTo;
import gov.nist.javax.sip.header.SIPHeaderNames;
import gov.nist.javax.sip.header.StatusLine;
import gov.nist.javax.sip.header.WWWAuthenticate;
import gov.nist.javax.sip.header.extensions.ReferredByHeader;
import gov.nist.javax.sip.header.extensions.ReplacesHeader;
import gov.nist.javax.sip.message.SIPMessage;
import gov.nist.javax.sip.message.SIPResponse;
import android.net.sip.ISipSession;
import android.net.sip.ISipSessionListener;
import android.net.sip.SipErrorCode;
import android.net.sip.SipProfile;
import android.net.sip.SipSession;
import android.net.sip.SipSessionAdapter;
import android.text.TextUtils;
import android.telephony.Rlog;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.DialogTerminatedEvent;
import javax.sip.IOExceptionEvent;
import javax.sip.ObjectInUseException;
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.ContactHeader;
import javax.sip.header.ExpiresHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.HeaderAddress;
import javax.sip.header.MinExpiresHeader;
import javax.sip.header.ReferToHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.Message;
import javax.sip.message.Request;
import javax.sip.message.Response;
/**
* Manages {@link ISipSession}'s for a SIP account.
*/
class SipSessionGroup implements SipListener {
private static final String TAG = "SipSession";
private static final boolean DBG = false;
private static final boolean DBG_PING = false;
private static final String ANONYMOUS = "anonymous";
// Limit the size of thread pool to 1 for the order issue when the phone is
// waken up from sleep and there are many packets to be processed in the SIP
// stack. Note: The default thread pool size in NIST SIP stack is -1 which is
// unlimited.
private static final String THREAD_POOL_SIZE = "1";
private static final int EXPIRY_TIME = 3600; // in seconds
private static final int CANCEL_CALL_TIMER = 3; // in seconds
private static final int END_CALL_TIMER = 3; // in seconds
private static final int KEEPALIVE_TIMEOUT = 5; // in seconds
private static final int INCALL_KEEPALIVE_INTERVAL = 10; // in seconds
private static final long WAKE_LOCK_HOLDING_TIME = 500; // in milliseconds
private static final EventObject DEREGISTER = new EventObject("Deregister");
private static final EventObject END_CALL = new EventObject("End call");
private final SipProfile mLocalProfile;
private final String mPassword;
private SipStack mSipStack;
private SipHelper mSipHelper;
// session that processes INVITE requests
private SipSessionImpl mCallReceiverSession;
private String mLocalIp;
private SipWakeupTimer mWakeupTimer;
private SipWakeLock mWakeLock;
// call-id-to-SipSession map
private Map<String, SipSessionImpl> mSessionMap =
new HashMap<String, SipSessionImpl>();
// external address observed from any response
private String mExternalIp;
private int mExternalPort;
/**
* @param profile the local profile with password crossed out
* @param password the password of the profile
* @throws SipException if cannot assign requested address
*/
public SipSessionGroup(SipProfile profile, String password,
SipWakeupTimer timer, SipWakeLock wakeLock) throws SipException {
mLocalProfile = profile;
mPassword = password;
mWakeupTimer = timer;
mWakeLock = wakeLock;
reset();
}
// TODO: remove this method once SipWakeupTimer can better handle variety
// of timeout values
void setWakeupTimer(SipWakeupTimer timer) {
mWakeupTimer = timer;
}
synchronized void reset() throws SipException {
Properties properties = new Properties();
String protocol = mLocalProfile.getProtocol();
int port = mLocalProfile.getPort();
String server = mLocalProfile.getProxyAddress();
if (!TextUtils.isEmpty(server)) {
properties.setProperty("javax.sip.OUTBOUND_PROXY",
server + ':' + port + '/' + protocol);
} else {
server = mLocalProfile.getSipDomain();
}
if (server.startsWith("[") && server.endsWith("]")) {
server = server.substring(1, server.length() - 1);
}
String local = null;
try {
for (InetAddress remote : InetAddress.getAllByName(server)) {
DatagramSocket socket = new DatagramSocket();
socket.connect(remote, port);
if (socket.isConnected()) {
local = socket.getLocalAddress().getHostAddress();
port = socket.getLocalPort();
socket.close();
break;
}
socket.close();
}
} catch (Exception e) {
// ignore.
}
if (local == null) {
// We are unable to reach the server. Just bail out.
return;
}
close();
mLocalIp = local;
properties.setProperty("javax.sip.STACK_NAME", getStackName());
properties.setProperty(
"gov.nist.javax.sip.THREAD_POOL_SIZE", THREAD_POOL_SIZE);
mSipStack = SipFactory.getInstance().createSipStack(properties);
try {
SipProvider provider = mSipStack.createSipProvider(
mSipStack.createListeningPoint(local, port, protocol));
provider.addSipListener(this);
mSipHelper = new SipHelper(mSipStack, provider);
} catch (SipException e) {
throw e;
} catch (Exception e) {
throw new SipException("failed to initialize SIP stack", e);
}
if (DBG) log("reset: start stack for " + mLocalProfile.getUriString());
mSipStack.start();
}
synchronized void onConnectivityChanged() {
SipSessionImpl[] ss = mSessionMap.values().toArray(
new SipSessionImpl[mSessionMap.size()]);
// Iterate on the copied array instead of directly on mSessionMap to
// avoid ConcurrentModificationException being thrown when
// SipSessionImpl removes itself from mSessionMap in onError() in the
// following loop.
for (SipSessionImpl s : ss) {
s.onError(SipErrorCode.DATA_CONNECTION_LOST,
"data connection lost");
}
}
synchronized void resetExternalAddress() {
if (DBG) {
log("resetExternalAddress: " + mSipStack);
}
mExternalIp = null;
mExternalPort = 0;
}
public SipProfile getLocalProfile() {
return mLocalProfile;
}
public String getLocalProfileUri() {
return mLocalProfile.getUriString();
}
private String getStackName() {
return "stack" + System.currentTimeMillis();
}
public synchronized void close() {
if (DBG) log("close: " + mLocalProfile.getUriString());
onConnectivityChanged();
mSessionMap.clear();
closeToNotReceiveCalls();
if (mSipStack != null) {
mSipStack.stop();
mSipStack = null;
mSipHelper = null;
}
resetExternalAddress();
}
public synchronized boolean isClosed() {
return (mSipStack == null);
}
// For internal use, require listener not to block in callbacks.
public synchronized void openToReceiveCalls(ISipSessionListener listener) {
if (mCallReceiverSession == null) {
mCallReceiverSession = new SipSessionCallReceiverImpl(listener);
} else {
mCallReceiverSession.setListener(listener);
}
}
public synchronized void closeToNotReceiveCalls() {
mCallReceiverSession = null;
}
public ISipSession createSession(ISipSessionListener listener) {
return (isClosed() ? null : new SipSessionImpl(listener));
}
synchronized boolean containsSession(String callId) {
return mSessionMap.containsKey(callId);
}
private synchronized SipSessionImpl getSipSession(EventObject event) {
String key = SipHelper.getCallId(event);
SipSessionImpl session = mSessionMap.get(key);
if ((session != null) && isLoggable(session)) {
if (DBG) log("getSipSession: event=" + key);
if (DBG) log("getSipSession: active sessions:");
for (String k : mSessionMap.keySet()) {
if (DBG) log("getSipSession: ..." + k + ": " + mSessionMap.get(k));
}
}
return ((session != null) ? session : mCallReceiverSession);
}
private synchronized void addSipSession(SipSessionImpl newSession) {
removeSipSession(newSession);
String key = newSession.getCallId();
mSessionMap.put(key, newSession);
if (isLoggable(newSession)) {
if (DBG) log("addSipSession: key='" + key + "'");
for (String k : mSessionMap.keySet()) {
if (DBG) log("addSipSession: " + k + ": " + mSessionMap.get(k));
}
}
}
private synchronized void removeSipSession(SipSessionImpl session) {
if (session == mCallReceiverSession) return;
String key = session.getCallId();
SipSessionImpl s = mSessionMap.remove(key);
// sanity check
if ((s != null) && (s != session)) {
if (DBG) log("removeSession: " + session + " is not associated with key '"
+ key + "'");
mSessionMap.put(key, s);
for (Map.Entry<String, SipSessionImpl> entry
: mSessionMap.entrySet()) {
if (entry.getValue() == s) {
key = entry.getKey();
mSessionMap.remove(key);
}
}
}
if ((s != null) && isLoggable(s)) {
if (DBG) log("removeSession: " + session + " @key '" + key + "'");
for (String k : mSessionMap.keySet()) {
if (DBG) log("removeSession: " + k + ": " + mSessionMap.get(k));
}
}
}
@Override
public void processRequest(final RequestEvent event) {
if (isRequestEvent(Request.INVITE, event)) {
if (DBG) log("processRequest: mWakeLock.acquire got INVITE, thread:"
+ Thread.currentThread());
// Acquire a wake lock and keep it for WAKE_LOCK_HOLDING_TIME;
// should be large enough to bring up the app.
mWakeLock.acquire(WAKE_LOCK_HOLDING_TIME);
}
process(event);
}
@Override
public void processResponse(ResponseEvent event) {
process(event);
}
@Override
public void processIOException(IOExceptionEvent event) {
process(event);
}
@Override
public void processTimeout(TimeoutEvent event) {
process(event);
}
@Override
public void processTransactionTerminated(TransactionTerminatedEvent event) {
process(event);
}
@Override
public void processDialogTerminated(DialogTerminatedEvent event) {
process(event);
}
private synchronized void process(EventObject event) {
SipSessionImpl session = getSipSession(event);
try {
boolean isLoggable = isLoggable(session, event);
boolean processed = (session != null) && session.process(event);
if (isLoggable && processed) {
log("process: event new state after: "
+ SipSession.State.toString(session.mState));
}
} catch (Throwable e) {
loge("process: error event=" + event, getRootCause(e));
session.onError(e);
}
}
private String extractContent(Message message) {
// Currently we do not support secure MIME bodies.
byte[] bytes = message.getRawContent();
if (bytes != null) {
try {
if (message instanceof SIPMessage) {
return ((SIPMessage) message).getMessageContent();
} else {
return new String(bytes, "UTF-8");
}
} catch (UnsupportedEncodingException e) {
}
}
return null;
}
private void extractExternalAddress(ResponseEvent evt) {
Response response = evt.getResponse();
ViaHeader viaHeader = (ViaHeader)(response.getHeader(
SIPHeaderNames.VIA));
if (viaHeader == null) return;
int rport = viaHeader.getRPort();
String externalIp = viaHeader.getReceived();
if ((rport > 0) && (externalIp != null)) {
mExternalIp = externalIp;
mExternalPort = rport;
if (DBG) {
log("extractExternalAddress: external addr " + externalIp + ":" + rport
+ " on " + mSipStack);
}
}
}
private Throwable getRootCause(Throwable exception) {
Throwable cause = exception.getCause();
while (cause != null) {
exception = cause;
cause = exception.getCause();
}
return exception;
}
private SipSessionImpl createNewSession(RequestEvent event,
ISipSessionListener listener, ServerTransaction transaction,
int newState) throws SipException {
SipSessionImpl newSession = new SipSessionImpl(listener);
newSession.mServerTransaction = transaction;
newSession.mState = newState;
newSession.mDialog = newSession.mServerTransaction.getDialog();
newSession.mInviteReceived = event;
newSession.mPeerProfile = createPeerProfile((HeaderAddress)
event.getRequest().getHeader(FromHeader.NAME));
newSession.mPeerSessionDescription =
extractContent(event.getRequest());
return newSession;
}
private class SipSessionCallReceiverImpl extends SipSessionImpl {
private static final String SSCRI_TAG = "SipSessionCallReceiverImpl";
private static final boolean SSCRI_DBG = true;
public SipSessionCallReceiverImpl(ISipSessionListener listener) {
super(listener);
}
private int processInviteWithReplaces(RequestEvent event,
ReplacesHeader replaces) {
String callId = replaces.getCallId();
SipSessionImpl session = mSessionMap.get(callId);
if (session == null) {
return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
}
Dialog dialog = session.mDialog;
if (dialog == null) return Response.DECLINE;
if (!dialog.getLocalTag().equals(replaces.getToTag()) ||
!dialog.getRemoteTag().equals(replaces.getFromTag())) {
// No match is found, returns 481.
return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
}
ReferredByHeader referredBy = (ReferredByHeader) event.getRequest()
.getHeader(ReferredByHeader.NAME);
if ((referredBy == null) ||
!dialog.getRemoteParty().equals(referredBy.getAddress())) {
return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
}
return Response.OK;
}
private void processNewInviteRequest(RequestEvent event)
throws SipException {
ReplacesHeader replaces = (ReplacesHeader) event.getRequest()
.getHeader(ReplacesHeader.NAME);
SipSessionImpl newSession = null;
if (replaces != null) {
int response = processInviteWithReplaces(event, replaces);
if (SSCRI_DBG) {
log("processNewInviteRequest: " + replaces
+ " response=" + response);
}
if (response == Response.OK) {
SipSessionImpl replacedSession =
mSessionMap.get(replaces.getCallId());
// got INVITE w/ replaces request.
newSession = createNewSession(event,
replacedSession.mProxy.getListener(),
mSipHelper.getServerTransaction(event),
SipSession.State.INCOMING_CALL);
newSession.mProxy.onCallTransferring(newSession,
newSession.mPeerSessionDescription);
} else {
mSipHelper.sendResponse(event, response);
}
} else {
// New Incoming call.
newSession = createNewSession(event, mProxy,
mSipHelper.sendRinging(event, generateTag()),
SipSession.State.INCOMING_CALL);
mProxy.onRinging(newSession, newSession.mPeerProfile,
newSession.mPeerSessionDescription);
}
if (newSession != null) addSipSession(newSession);
}
@Override
public boolean process(EventObject evt) throws SipException {
if (isLoggable(this, evt)) log("process: " + this + ": "
+ SipSession.State.toString(mState) + ": processing "
+ logEvt(evt));
if (isRequestEvent(Request.INVITE, evt)) {
processNewInviteRequest((RequestEvent) evt);
return true;
} else if (isRequestEvent(Request.OPTIONS, evt)) {
mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
return true;
} else {
return false;
}
}
private void log(String s) {
Rlog.d(SSCRI_TAG, s);
}
}
static interface KeepAliveProcessCallback {
/** Invoked when the response of keeping alive comes back. */
void onResponse(boolean portChanged);
void onError(int errorCode, String description);
}
class SipSessionImpl extends ISipSession.Stub {
private static final String SSI_TAG = "SipSessionImpl";
private static final boolean SSI_DBG = true;
SipProfile mPeerProfile;
SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
int mState = SipSession.State.READY_TO_CALL;
RequestEvent mInviteReceived;
Dialog mDialog;
ServerTransaction mServerTransaction;
ClientTransaction mClientTransaction;
String mPeerSessionDescription;
boolean mInCall;
SessionTimer mSessionTimer;
int mAuthenticationRetryCount;
private SipKeepAlive mSipKeepAlive;
private SipSessionImpl mSipSessionImpl;
// the following three members are used for handling refer request.
SipSessionImpl mReferSession;
ReferredByHeader mReferredBy;
String mReplaces;
// lightweight timer
class SessionTimer {
private boolean mRunning = true;
void start(final int timeout) {
new Thread(new Runnable() {
@Override
public void run() {
sleep(timeout);
if (mRunning) timeout();
}
}, "SipSessionTimerThread").start();
}
synchronized void cancel() {
mRunning = false;
this.notify();
}
private void timeout() {
synchronized (SipSessionGroup.this) {
onError(SipErrorCode.TIME_OUT, "Session timed out!");
}
}
private synchronized void sleep(int timeout) {
try {
this.wait(timeout * 1000);
} catch (InterruptedException e) {
loge("session timer interrupted!", e);
}
}
}
public SipSessionImpl(ISipSessionListener listener) {
setListener(listener);
}
SipSessionImpl duplicate() {
return new SipSessionImpl(mProxy.getListener());
}
private void reset() {
mInCall = false;
removeSipSession(this);
mPeerProfile = null;
mState = SipSession.State.READY_TO_CALL;
mInviteReceived = null;
mPeerSessionDescription = null;
mAuthenticationRetryCount = 0;
mReferSession = null;
mReferredBy = null;
mReplaces = null;
if (mDialog != null) mDialog.delete();
mDialog = null;
try {
if (mServerTransaction != null) mServerTransaction.terminate();
} catch (ObjectInUseException e) {
// ignored
}
mServerTransaction = null;
try {
if (mClientTransaction != null) mClientTransaction.terminate();
} catch (ObjectInUseException e) {
// ignored
}
mClientTransaction = null;
cancelSessionTimer();
if (mSipSessionImpl != null) {
mSipSessionImpl.stopKeepAliveProcess();
mSipSessionImpl = null;
}
}
@Override
public boolean isInCall() {
return mInCall;
}
@Override
public String getLocalIp() {
return mLocalIp;
}
@Override
public SipProfile getLocalProfile() {
return mLocalProfile;
}
@Override
public SipProfile getPeerProfile() {
return mPeerProfile;
}
@Override
public String getCallId() {
return SipHelper.getCallId(getTransaction());
}
private Transaction getTransaction() {
if (mClientTransaction != null) return mClientTransaction;
if (mServerTransaction != null) return mServerTransaction;
return null;
}
@Override
public int getState() {
return mState;
}
@Override
public void setListener(ISipSessionListener listener) {
mProxy.setListener((listener instanceof SipSessionListenerProxy)
? ((SipSessionListenerProxy) listener).getListener()
: listener);
}
// process the command in a new thread
private void doCommandAsync(final EventObject command) {
new Thread(new Runnable() {
@Override
public void run() {
try {
processCommand(command);
} catch (Throwable e) {
loge("command error: " + command + ": "
+ mLocalProfile.getUriString(),
getRootCause(e));
onError(e);
}
}
}, "SipSessionAsyncCmdThread").start();
}
@Override
public void makeCall(SipProfile peerProfile, String sessionDescription,
int timeout) {
doCommandAsync(new MakeCallCommand(peerProfile, sessionDescription,
timeout));
}
@Override
public void answerCall(String sessionDescription, int timeout) {
synchronized (SipSessionGroup.this) {
if (mPeerProfile == null) return;
doCommandAsync(new MakeCallCommand(mPeerProfile,
sessionDescription, timeout));
}
}
@Override
public void endCall() {
doCommandAsync(END_CALL);
}
@Override
public void changeCall(String sessionDescription, int timeout) {
synchronized (SipSessionGroup.this) {
if (mPeerProfile == null) return;
doCommandAsync(new MakeCallCommand(mPeerProfile,
sessionDescription, timeout));
}
}
@Override
public void register(int duration) {
doCommandAsync(new RegisterCommand(duration));
}
@Override
public void unregister() {
doCommandAsync(DEREGISTER);
}
private void processCommand(EventObject command) throws SipException {
if (isLoggable(command)) log("process cmd: " + command);
if (!process(command)) {
onError(SipErrorCode.IN_PROGRESS,
"cannot initiate a new transaction to execute: "
+ command);
}
}
protected String generateTag() {
// 32-bit randomness
return String.valueOf((long) (Math.random() * 0x100000000L));
}
@Override
public String toString() {
try {
String s = super.toString();
return s.substring(s.indexOf("@")) + ":"
+ SipSession.State.toString(mState);
} catch (Throwable e) {
return super.toString();
}
}
public boolean process(EventObject evt) throws SipException {
if (isLoggable(this, evt)) log(" ~~~~~ " + this + ": "
+ SipSession.State.toString(mState) + ": processing "
+ logEvt(evt));
synchronized (SipSessionGroup.this) {
if (isClosed()) return false;
if (mSipKeepAlive != null) {
// event consumed by keepalive process
if (mSipKeepAlive.process(evt)) return true;
}
Dialog dialog = null;
if (evt instanceof RequestEvent) {
dialog = ((RequestEvent) evt).getDialog();
} else if (evt instanceof ResponseEvent) {
dialog = ((ResponseEvent) evt).getDialog();
extractExternalAddress((ResponseEvent) evt);
}
if (dialog != null) mDialog = dialog;
boolean processed;
switch (mState) {
case SipSession.State.REGISTERING:
case SipSession.State.DEREGISTERING:
processed = registeringToReady(evt);
break;
case SipSession.State.READY_TO_CALL:
processed = readyForCall(evt);
break;
case SipSession.State.INCOMING_CALL:
processed = incomingCall(evt);
break;
case SipSession.State.INCOMING_CALL_ANSWERING:
processed = incomingCallToInCall(evt);
break;
case SipSession.State.OUTGOING_CALL:
case SipSession.State.OUTGOING_CALL_RING_BACK:
processed = outgoingCall(evt);
break;
case SipSession.State.OUTGOING_CALL_CANCELING:
processed = outgoingCallToReady(evt);
break;
case SipSession.State.IN_CALL:
processed = inCall(evt);
break;
case SipSession.State.ENDING_CALL:
processed = endingCall(evt);
break;
default:
processed = false;
}
return (processed || processExceptions(evt));
}
}
private boolean processExceptions(EventObject evt) throws SipException {
if (isRequestEvent(Request.BYE, evt)) {
// terminate the call whenever a BYE is received
mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
endCallNormally();
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 (isCurrentTransaction((TransactionTerminatedEvent) evt)) {
if (evt instanceof TimeoutEvent) {
processTimeout((TimeoutEvent) evt);
} else {
processTransactionTerminated(
(TransactionTerminatedEvent) evt);
}
return true;
}
} else if (isRequestEvent(Request.OPTIONS, evt)) {
mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
return true;
} else if (evt instanceof DialogTerminatedEvent) {
processDialogTerminated((DialogTerminatedEvent) evt);
return true;
}
return false;
}
private void processDialogTerminated(DialogTerminatedEvent event) {
if (mDialog == event.getDialog()) {
onError(new SipException("dialog terminated"));
} else {
if (SSI_DBG) log("not the current dialog; current=" + mDialog
+ ", terminated=" + event.getDialog());
}
}
private boolean isCurrentTransaction(TransactionTerminatedEvent event) {
Transaction current = event.isServerTransaction()
? mServerTransaction
: mClientTransaction;
Transaction target = event.isServerTransaction()
? event.getServerTransaction()
: event.getClientTransaction();
if ((current != target) && (mState != SipSession.State.PINGING)) {
if (SSI_DBG) log("not the current transaction; current="
+ toString(current) + ", target=" + toString(target));
return false;
} else if (current != null) {
if (SSI_DBG) log("transaction terminated: " + toString(current));
return true;
} else {
// no transaction; shouldn't be here; ignored
return true;
}
}
private String toString(Transaction transaction) {
if (transaction == null) return "null";
Request request = transaction.getRequest();
Dialog dialog = transaction.getDialog();
CSeqHeader cseq = (CSeqHeader) request.getHeader(CSeqHeader.NAME);
return String.format("req=%s,%s,s=%s,ds=%s,", request.getMethod(),
cseq.getSeqNumber(), transaction.getState(),
((dialog == null) ? "-" : dialog.getState()));
}
private void processTransactionTerminated(
TransactionTerminatedEvent event) {
switch (mState) {
case SipSession.State.IN_CALL:
case SipSession.State.READY_TO_CALL:
if (SSI_DBG) log("Transaction terminated; do nothing");
break;
default:
if (SSI_DBG) log("Transaction terminated early: " + this);
onError(SipErrorCode.TRANSACTION_TERMINTED,
"transaction terminated");
}
}
private void processTimeout(TimeoutEvent event) {
if (SSI_DBG) log("processing Timeout...");
switch (mState) {
case SipSession.State.REGISTERING:
case SipSession.State.DEREGISTERING:
reset();
mProxy.onRegistrationTimeout(this);
break;
case SipSession.State.INCOMING_CALL:
case SipSession.State.INCOMING_CALL_ANSWERING:
case SipSession.State.OUTGOING_CALL:
case SipSession.State.OUTGOING_CALL_CANCELING:
onError(SipErrorCode.TIME_OUT, event.toString());
break;
default:
if (SSI_DBG) log(" do nothing");
break;
}
}
private int getExpiryTime(Response response) {
int time = -1;
ContactHeader contact = (ContactHeader) response.getHeader(ContactHeader.NAME);
if (contact != null) {
time = contact.getExpires();
}
ExpiresHeader expires = (ExpiresHeader) response.getHeader(ExpiresHeader.NAME);
if (expires != null && (time < 0 || time > expires.getExpires())) {
time = expires.getExpires();
}
if (time <= 0) {
time = EXPIRY_TIME;
}
expires = (ExpiresHeader) response.getHeader(MinExpiresHeader.NAME);
if (expires != null && time < expires.getExpires()) {
time = expires.getExpires();
}
if (SSI_DBG) {
log("Expiry time = " + time);
}
return time;
}
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:
int state = mState;
onRegistrationDone((state == SipSession.State.REGISTERING)
? getExpiryTime(((ResponseEvent) evt).getResponse())
: -1);
return true;
case Response.UNAUTHORIZED:
case Response.PROXY_AUTHENTICATION_REQUIRED:
handleAuthentication(event);
return true;
default:
if (statusCode >= 500) {
onRegistrationFailed(response);
return true;
}
}
}
return false;
}
private boolean handleAuthentication(ResponseEvent event)
throws SipException {
Response response = event.getResponse();
String nonce = getNonceFromResponse(response);
if (nonce == null) {
onError(SipErrorCode.SERVER_ERROR,
"server does not provide challenge");
return false;
} else if (mAuthenticationRetryCount < 2) {
mClientTransaction = mSipHelper.handleChallenge(
event, getAccountManager());
mDialog = mClientTransaction.getDialog();
mAuthenticationRetryCount++;
if (isLoggable(this, event)) {
if (SSI_DBG) log(" authentication retry count="
+ mAuthenticationRetryCount);
}
return true;
} else {
if (crossDomainAuthenticationRequired(response)) {
onError(SipErrorCode.CROSS_DOMAIN_AUTHENTICATION,
getRealmFromResponse(response));
} else {
onError(SipErrorCode.INVALID_CREDENTIALS,
"incorrect username or password");
}
return false;
}
}
private boolean crossDomainAuthenticationRequired(Response response) {
String realm = getRealmFromResponse(response);
if (realm == null) realm = "";
return !mLocalProfile.getSipDomain().trim().equals(realm.trim());
}
private AccountManager getAccountManager() {
return new AccountManager() {
@Override
public UserCredentials getCredentials(ClientTransaction
challengedTransaction, String realm) {
return new UserCredentials() {
@Override
public String getUserName() {
String username = mLocalProfile.getAuthUserName();
return (!TextUtils.isEmpty(username) ? username :
mLocalProfile.getUserName());
}
@Override
public String getPassword() {
return mPassword;
}
@Override
public String getSipDomain() {
return mLocalProfile.getSipDomain();
}
};
}
};
}
private String getRealmFromResponse(Response response) {
WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
SIPHeaderNames.WWW_AUTHENTICATE);
if (wwwAuth != null) return wwwAuth.getRealm();
ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
SIPHeaderNames.PROXY_AUTHENTICATE);
return (proxyAuth == null) ? null : proxyAuth.getRealm();
}
private String getNonceFromResponse(Response response) {
WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
SIPHeaderNames.WWW_AUTHENTICATE);
if (wwwAuth != null) return wwwAuth.getNonce();
ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
SIPHeaderNames.PROXY_AUTHENTICATE);
return (proxyAuth == null) ? null : proxyAuth.getNonce();
}
private String getResponseString(int statusCode) {
StatusLine statusLine = new StatusLine();
statusLine.setStatusCode(statusCode);
statusLine.setReasonPhrase(SIPResponse.getReasonPhrase(statusCode));
return statusLine.encode();
}
private boolean readyForCall(EventObject evt) throws SipException {
// expect MakeCallCommand, RegisterCommand, DEREGISTER
if (evt instanceof MakeCallCommand) {
mState = SipSession.State.OUTGOING_CALL;
MakeCallCommand cmd = (MakeCallCommand) evt;
mPeerProfile = cmd.getPeerProfile();
if (mReferSession != null) {
mSipHelper.sendReferNotify(mReferSession.mDialog,
getResponseString(Response.TRYING));
}
mClientTransaction = mSipHelper.sendInvite(
mLocalProfile, mPeerProfile, cmd.getSessionDescription(),
generateTag(), mReferredBy, mReplaces);
mDialog = mClientTransaction.getDialog();
addSipSession(this);
startSessionTimer(cmd.getTimeout());
mProxy.onCalling(this);
return true;
} else if (evt instanceof RegisterCommand) {
mState = SipSession.State.REGISTERING;
int duration = ((RegisterCommand) evt).getDuration();
mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
generateTag(), duration);
mDialog = mClientTransaction.getDialog();
addSipSession(this);
mProxy.onRegistering(this);
return true;
} else if (DEREGISTER == evt) {
mState = SipSession.State.DEREGISTERING;
mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
generateTag(), 0);
mDialog = mClientTransaction.getDialog();
addSipSession(this);
mProxy.onRegistering(this);
return true;
}
return false;
}
private boolean incomingCall(EventObject evt) throws SipException {
// expect MakeCallCommand(answering) , END_CALL cmd , Cancel
if (evt instanceof MakeCallCommand) {
// answer call
mState = SipSession.State.INCOMING_CALL_ANSWERING;
mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived,
mLocalProfile,
((MakeCallCommand) evt).getSessionDescription(),
mServerTransaction,
mExternalIp, mExternalPort);
startSessionTimer(((MakeCallCommand) evt).getTimeout());
return true;
} else if (END_CALL == evt) {
mSipHelper.sendInviteBusyHere(mInviteReceived,
mServerTransaction);
endCallNormally();
return true;
} else if (isRequestEvent(Request.CANCEL, evt)) {
RequestEvent event = (RequestEvent) evt;
mSipHelper.sendResponse(event, Response.OK);
mSipHelper.sendInviteRequestTerminated(
mInviteReceived.getRequest(), mServerTransaction);
endCallNormally();
return true;
}
return false;
}
private boolean incomingCallToInCall(EventObject evt) {
// expect ACK, CANCEL request
if (isRequestEvent(Request.ACK, evt)) {
String sdp = extractContent(((RequestEvent) evt).getRequest());
if (sdp != null) mPeerSessionDescription = sdp;
if (mPeerSessionDescription == null) {
onError(SipErrorCode.CLIENT_ERROR, "peer sdp is empty");
} else {
establishCall(false);
}
return true;
} else if (isRequestEvent(Request.CANCEL, evt)) {
// http://tools.ietf.org/html/rfc3261#section-9.2
// Final response has been sent; do nothing here.
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:
case Response.CALL_IS_BEING_FORWARDED:
case Response.QUEUED:
case Response.SESSION_PROGRESS:
// feedback any provisional responses (except TRYING) as
// ring back for better UX
if (mState == SipSession.State.OUTGOING_CALL) {
mState = SipSession.State.OUTGOING_CALL_RING_BACK;
cancelSessionTimer();
mProxy.onRingingBack(this);
}
return true;
case Response.OK:
if (mReferSession != null) {
mSipHelper.sendReferNotify(mReferSession.mDialog,
getResponseString(Response.OK));
// since we don't need to remember the session anymore.
mReferSession = null;
}
mSipHelper.sendInviteAck(event, mDialog);
mPeerSessionDescription = extractContent(response);
establishCall(true);
return true;
case Response.UNAUTHORIZED:
case Response.PROXY_AUTHENTICATION_REQUIRED:
if (handleAuthentication(event)) {
addSipSession(this);
}
return true;
case Response.REQUEST_PENDING:
// TODO: rfc3261#section-14.1; re-schedule invite
return true;
default:
if (mReferSession != null) {
mSipHelper.sendReferNotify(mReferSession.mDialog,
getResponseString(Response.SERVICE_UNAVAILABLE));
}
if (statusCode >= 400) {
// error: an ack is sent automatically by the stack
onError(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.
mState = SipSession.State.OUTGOING_CALL_CANCELING;
mSipHelper.sendCancel(mClientTransaction);
startSessionTimer(CANCEL_CALL_TIMER);
return true;
} else if (isRequestEvent(Request.INVITE, evt)) {
// Call self? Send BUSY HERE so server may redirect the call to
// voice mailbox.
RequestEvent event = (RequestEvent) evt;
mSipHelper.sendInviteBusyHere(event,
event.getServerTransaction());
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)) {
switch (statusCode) {
case Response.OK:
outgoingCall(evt); // abort Cancel
return true;
case Response.REQUEST_TERMINATED:
endCallNormally();
return true;
}
} else {
return false;
}
if (statusCode >= 400) {
onError(response);
return true;
}
} else if (evt instanceof TransactionTerminatedEvent) {
// rfc3261#section-14.1:
// if re-invite gets timed out, terminate the dialog; but
// re-invite is not reliable, just let it go and pretend
// nothing happened.
onError(new SipException("timed out"));
}
return false;
}
private boolean processReferRequest(RequestEvent event)
throws SipException {
try {
ReferToHeader referto = (ReferToHeader) event.getRequest()
.getHeader(ReferTo.NAME);
Address address = referto.getAddress();
SipURI uri = (SipURI) address.getURI();
String replacesHeader = uri.getHeader(ReplacesHeader.NAME);
String username = uri.getUser();
if (username == null) {
mSipHelper.sendResponse(event, Response.BAD_REQUEST);
return false;
}
// send notify accepted
mSipHelper.sendResponse(event, Response.ACCEPTED);
SipSessionImpl newSession = createNewSession(event,
this.mProxy.getListener(),
mSipHelper.getServerTransaction(event),
SipSession.State.READY_TO_CALL);
newSession.mReferSession = this;
newSession.mReferredBy = (ReferredByHeader) event.getRequest()
.getHeader(ReferredByHeader.NAME);
newSession.mReplaces = replacesHeader;
newSession.mPeerProfile = createPeerProfile(referto);
newSession.mProxy.onCallTransferring(newSession,
null);
return true;
} catch (IllegalArgumentException e) {
throw new SipException("createPeerProfile()", e);
}
}
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
mState = SipSession.State.ENDING_CALL;
mSipHelper.sendBye(mDialog);
mProxy.onCallEnded(this);
startSessionTimer(END_CALL_TIMER);
return true;
} else if (isRequestEvent(Request.INVITE, evt)) {
// got Re-INVITE
mState = SipSession.State.INCOMING_CALL;
RequestEvent event = mInviteReceived = (RequestEvent) evt;
mPeerSessionDescription = extractContent(event.getRequest());
mServerTransaction = null;
mProxy.onRinging(this, mPeerProfile, mPeerSessionDescription);
return true;
} else if (isRequestEvent(Request.BYE, evt)) {
mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
endCallNormally();
return true;
} else if (isRequestEvent(Request.REFER, evt)) {
return processReferRequest((RequestEvent) evt);
} else if (evt instanceof MakeCallCommand) {
// to change call
mState = SipSession.State.OUTGOING_CALL;
mClientTransaction = mSipHelper.sendReinvite(mDialog,
((MakeCallCommand) evt).getSessionDescription());
startSessionTimer(((MakeCallCommand) evt).getTimeout());
return true;
} else if (evt instanceof ResponseEvent) {
if (expectResponse(Request.NOTIFY, evt)) return true;
}
return false;
}
private boolean endingCall(EventObject evt) throws SipException {
if (expectResponse(Request.BYE, evt)) {
ResponseEvent event = (ResponseEvent) evt;
Response response = event.getResponse();
int statusCode = response.getStatusCode();
switch (statusCode) {
case Response.UNAUTHORIZED:
case Response.PROXY_AUTHENTICATION_REQUIRED:
if (handleAuthentication(event)) {
return true;
} else {
// can't authenticate; pass through to end session
}
}
cancelSessionTimer();
reset();
return true;
}
return false;
}
// timeout in seconds
private void startSessionTimer(int timeout) {
if (timeout > 0) {
mSessionTimer = new SessionTimer();
mSessionTimer.start(timeout);
}
}
private void cancelSessionTimer() {
if (mSessionTimer != null) {
mSessionTimer.cancel();
mSessionTimer = null;
}
}
private String createErrorMessage(Response response) {
return String.format("%s (%d)", response.getReasonPhrase(),
response.getStatusCode());
}
private void enableKeepAlive() {
if (mSipSessionImpl != null) {
mSipSessionImpl.stopKeepAliveProcess();
} else {
mSipSessionImpl = duplicate();
}
try {
mSipSessionImpl.startKeepAliveProcess(
INCALL_KEEPALIVE_INTERVAL, mPeerProfile, null);
} catch (SipException e) {
loge("keepalive cannot be enabled; ignored", e);
mSipSessionImpl.stopKeepAliveProcess();
}
}
private void establishCall(boolean enableKeepAlive) {
mState = SipSession.State.IN_CALL;
cancelSessionTimer();
if (!mInCall && enableKeepAlive) enableKeepAlive();
mInCall = true;
mProxy.onCallEstablished(this, mPeerSessionDescription);
}
private void endCallNormally() {
reset();
mProxy.onCallEnded(this);
}
private void endCallOnError(int errorCode, String message) {
reset();
mProxy.onError(this, errorCode, message);
}
private void endCallOnBusy() {
reset();
mProxy.onCallBusy(this);
}
private void onError(int errorCode, String message) {
cancelSessionTimer();
switch (mState) {
case SipSession.State.REGISTERING:
case SipSession.State.DEREGISTERING:
onRegistrationFailed(errorCode, message);
break;
default:
endCallOnError(errorCode, message);
}
}
private void onError(Throwable exception) {
exception = getRootCause(exception);
onError(getErrorCode(exception), exception.toString());
}
private void onError(Response response) {
int statusCode = response.getStatusCode();
if (!mInCall && (statusCode == Response.BUSY_HERE)) {
endCallOnBusy();
} else {
onError(getErrorCode(statusCode), createErrorMessage(response));
}
}
private int getErrorCode(int responseStatusCode) {
switch (responseStatusCode) {
case Response.TEMPORARILY_UNAVAILABLE:
case Response.FORBIDDEN:
case Response.GONE:
case Response.NOT_FOUND:
case Response.NOT_ACCEPTABLE:
case Response.NOT_ACCEPTABLE_HERE:
return SipErrorCode.PEER_NOT_REACHABLE;
case Response.REQUEST_URI_TOO_LONG:
case Response.ADDRESS_INCOMPLETE:
case Response.AMBIGUOUS:
return SipErrorCode.INVALID_REMOTE_URI;
case Response.REQUEST_TIMEOUT:
return SipErrorCode.TIME_OUT;
default:
if (responseStatusCode < 500) {
return SipErrorCode.CLIENT_ERROR;
} else {
return SipErrorCode.SERVER_ERROR;
}
}
}
private int getErrorCode(Throwable exception) {
String message = exception.getMessage();
if (exception instanceof UnknownHostException) {
return SipErrorCode.SERVER_UNREACHABLE;
} else if (exception instanceof IOException) {
return SipErrorCode.SOCKET_ERROR;
} else {
return SipErrorCode.CLIENT_ERROR;
}
}
private void onRegistrationDone(int duration) {
reset();
mProxy.onRegistrationDone(this, duration);
}
private void onRegistrationFailed(int errorCode, String message) {
reset();
mProxy.onRegistrationFailed(this, errorCode, message);
}
private void onRegistrationFailed(Response response) {
int statusCode = response.getStatusCode();
onRegistrationFailed(getErrorCode(statusCode),
createErrorMessage(response));
}
// Notes: SipSessionListener will be replaced by the keepalive process
// @param interval in seconds
public void startKeepAliveProcess(int interval,
KeepAliveProcessCallback callback) throws SipException {
synchronized (SipSessionGroup.this) {
startKeepAliveProcess(interval, mLocalProfile, callback);
}
}
// Notes: SipSessionListener will be replaced by the keepalive process
// @param interval in seconds
public void startKeepAliveProcess(int interval, SipProfile peerProfile,
KeepAliveProcessCallback callback) throws SipException {
synchronized (SipSessionGroup.this) {
if (mSipKeepAlive != null) {
throw new SipException("Cannot create more than one "
+ "keepalive process in a SipSession");
}
mPeerProfile = peerProfile;
mSipKeepAlive = new SipKeepAlive();
mProxy.setListener(mSipKeepAlive);
mSipKeepAlive.start(interval, callback);
}
}
public void stopKeepAliveProcess() {
synchronized (SipSessionGroup.this) {
if (mSipKeepAlive != null) {
mSipKeepAlive.stop();
mSipKeepAlive = null;
}
}
}
class SipKeepAlive extends SipSessionAdapter implements Runnable {
private static final String SKA_TAG = "SipKeepAlive";
private static final boolean SKA_DBG = true;
private boolean mRunning = false;
private KeepAliveProcessCallback mCallback;
private boolean mPortChanged = false;
private int mRPort = 0;
private int mInterval; // just for debugging
// @param interval in seconds
void start(int interval, KeepAliveProcessCallback callback) {
if (mRunning) return;
mRunning = true;
mInterval = interval;
mCallback = new KeepAliveProcessCallbackProxy(callback);
mWakeupTimer.set(interval * 1000, this);
if (SKA_DBG) {
log("start keepalive:"
+ mLocalProfile.getUriString());
}
// No need to run the first time in a separate thread for now
run();
}
// return true if the event is consumed
boolean process(EventObject evt) {
if (mRunning && (mState == SipSession.State.PINGING)) {
if (evt instanceof ResponseEvent) {
if (parseOptionsResult(evt)) {
if (mPortChanged) {
resetExternalAddress();
stop();
} else {
cancelSessionTimer();
removeSipSession(SipSessionImpl.this);
}
mCallback.onResponse(mPortChanged);
return true;
}
}
}
return false;
}
// SipSessionAdapter
// To react to the session timeout event and network error.
@Override
public void onError(ISipSession session, int errorCode, String message) {
stop();
mCallback.onError(errorCode, message);
}
// SipWakeupTimer timeout handler
// To send out keepalive message.
@Override
public void run() {
synchronized (SipSessionGroup.this) {
if (!mRunning) return;
if (DBG_PING) {
String peerUri = (mPeerProfile == null)
? "null"
: mPeerProfile.getUriString();
log("keepalive: " + mLocalProfile.getUriString()
+ " --> " + peerUri + ", interval=" + mInterval);
}
try {
sendKeepAlive();
} catch (Throwable t) {
if (SKA_DBG) {
loge("keepalive error: "
+ mLocalProfile.getUriString(), getRootCause(t));
}
// It's possible that the keepalive process is being stopped
// during session.sendKeepAlive() so need to check mRunning
// again here.
if (mRunning) SipSessionImpl.this.onError(t);
}
}
}
void stop() {
synchronized (SipSessionGroup.this) {
if (SKA_DBG) {
log("stop keepalive:" + mLocalProfile.getUriString()
+ ",RPort=" + mRPort);
}
mRunning = false;
mWakeupTimer.cancel(this);
reset();
}
}
private void sendKeepAlive() throws SipException {
synchronized (SipSessionGroup.this) {
mState = SipSession.State.PINGING;
mClientTransaction = mSipHelper.sendOptions(
mLocalProfile, mPeerProfile, generateTag());
mDialog = mClientTransaction.getDialog();
addSipSession(SipSessionImpl.this);
startSessionTimer(KEEPALIVE_TIMEOUT);
// when timed out, onError() will be called with SipErrorCode.TIME_OUT
}
}
private boolean parseOptionsResult(EventObject evt) {
if (expectResponse(Request.OPTIONS, evt)) {
ResponseEvent event = (ResponseEvent) evt;
int rPort = getRPortFromResponse(event.getResponse());
if (rPort != -1) {
if (mRPort == 0) mRPort = rPort;
if (mRPort != rPort) {
mPortChanged = true;
if (SKA_DBG) log(String.format(
"rport is changed: %d <> %d", mRPort, rPort));
mRPort = rPort;
} else {
if (SKA_DBG) log("rport is the same: " + rPort);
}
} else {
if (SKA_DBG) log("peer did not respond rport");
}
return true;
}
return false;
}
private int getRPortFromResponse(Response response) {
ViaHeader viaHeader = (ViaHeader)(response.getHeader(
SIPHeaderNames.VIA));
return (viaHeader == null) ? -1 : viaHeader.getRPort();
}
private void log(String s) {
Rlog.d(SKA_TAG, s);
}
}
private void log(String s) {
Rlog.d(SSI_TAG, s);
}
}
/**
* @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;
}
private static SipProfile createPeerProfile(HeaderAddress header)
throws SipException {
try {
Address address = header.getAddress();
SipURI uri = (SipURI) address.getURI();
String username = uri.getUser();
if (username == null) username = ANONYMOUS;
int port = uri.getPort();
SipProfile.Builder builder =
new SipProfile.Builder(username, uri.getHost())
.setDisplayName(address.getDisplayName());
if (port > 0) builder.setPort(port);
return builder.build();
} catch (IllegalArgumentException e) {
throw new SipException("createPeerProfile()", e);
} catch (ParseException e) {
throw new SipException("createPeerProfile()", e);
}
}
private static boolean isLoggable(SipSessionImpl s) {
if (s != null) {
switch (s.mState) {
case SipSession.State.PINGING:
return DBG_PING;
}
}
return DBG;
}
private static boolean isLoggable(EventObject evt) {
return isLoggable(null, evt);
}
private static boolean isLoggable(SipSessionImpl s, EventObject evt) {
if (!isLoggable(s)) return false;
if (evt == null) return false;
if (evt instanceof ResponseEvent) {
Response response = ((ResponseEvent) evt).getResponse();
if (Request.OPTIONS.equals(response.getHeader(CSeqHeader.NAME))) {
return DBG_PING;
}
return DBG;
} else if (evt instanceof RequestEvent) {
if (isRequestEvent(Request.OPTIONS, evt)) {
return DBG_PING;
}
return DBG;
}
return false;
}
private static String logEvt(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();
}
}
private class RegisterCommand extends EventObject {
private int mDuration;
public RegisterCommand(int duration) {
super(SipSessionGroup.this);
mDuration = duration;
}
public int getDuration() {
return mDuration;
}
}
private class MakeCallCommand extends EventObject {
private String mSessionDescription;
private int mTimeout; // in seconds
public MakeCallCommand(SipProfile peerProfile,
String sessionDescription, int timeout) {
super(peerProfile);
mSessionDescription = sessionDescription;
mTimeout = timeout;
}
public SipProfile getPeerProfile() {
return (SipProfile) getSource();
}
public String getSessionDescription() {
return mSessionDescription;
}
public int getTimeout() {
return mTimeout;
}
}
/** Class to help safely run KeepAliveProcessCallback in a different thread. */
static class KeepAliveProcessCallbackProxy implements KeepAliveProcessCallback {
private static final String KAPCP_TAG = "KeepAliveProcessCallbackProxy";
private KeepAliveProcessCallback mCallback;
KeepAliveProcessCallbackProxy(KeepAliveProcessCallback callback) {
mCallback = callback;
}
private void proxy(Runnable runnable) {
// One thread for each calling back.
// Note: Guarantee ordering if the issue becomes important. Currently,
// the chance of handling two callback events at a time is none.
new Thread(runnable, "SIP-KeepAliveProcessCallbackThread").start();
}
@Override
public void onResponse(final boolean portChanged) {
if (mCallback == null) return;
proxy(new Runnable() {
@Override
public void run() {
try {
mCallback.onResponse(portChanged);
} catch (Throwable t) {
loge("onResponse", t);
}
}
});
}
@Override
public void onError(final int errorCode, final String description) {
if (mCallback == null) return;
proxy(new Runnable() {
@Override
public void run() {
try {
mCallback.onError(errorCode, description);
} catch (Throwable t) {
loge("onError", t);
}
}
});
}
private void loge(String s, Throwable t) {
Rlog.e(KAPCP_TAG, s, t);
}
}
private void log(String s) {
Rlog.d(TAG, s);
}
private void loge(String s, Throwable t) {
Rlog.e(TAG, s, t);
}
}