/* * 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.SIPHeaderNames; import gov.nist.javax.sip.header.ProxyAuthenticate; import gov.nist.javax.sip.header.WWWAuthenticate; import gov.nist.javax.sip.message.SIPMessage; import android.net.sip.ISipSession; import android.net.sip.ISipSessionListener; import android.net.sip.SessionDescription; import android.net.sip.SipErrorCode; import android.net.sip.SipProfile; import android.net.sip.SipSessionAdapter; import android.net.sip.SipSessionState; import android.text.TextUtils; import android.util.Log; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.DatagramSocket; import java.net.UnknownHostException; import java.text.ParseException; import java.util.Collection; import java.util.EventObject; import java.util.HashMap; import java.util.Map; import java.util.Properties; 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.TransactionState; import javax.sip.TransactionTerminatedEvent; import javax.sip.TransactionUnavailableException; 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.header.MinExpiresHeader; 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 String ANONYMOUS = "anonymous"; private static final String SERVER_ERROR_PREFIX = "Response: "; private static final int EXPIRY_TIME = 3600; 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 final SipProfile mLocalProfile; private final String mPassword; private SipStack mSipStack; private SipHelper mSipHelper; private String mLastNonce; private int mRPort; // session that processes INVITE requests private SipSessionImpl mCallReceiverSession; private String mLocalIp; // call-id-to-SipSession map private Map mSessionMap = new HashMap(); /** * @param myself the local profile with password crossed out * @param password the password of the profile * @throws IOException if cannot assign requested address */ public SipSessionGroup(String localIp, SipProfile myself, String password) throws SipException, IOException { mLocalProfile = myself; mPassword = password; reset(localIp); } void reset(String localIp) throws SipException, IOException { mLocalIp = localIp; if (localIp == null) return; SipProfile myself = mLocalProfile; SipFactory sipFactory = SipFactory.getInstance(); Properties properties = new Properties(); properties.setProperty("javax.sip.STACK_NAME", getStackName()); String outboundProxy = myself.getProxyAddress(); if (!TextUtils.isEmpty(outboundProxy)) { Log.v(TAG, "outboundProxy is " + outboundProxy); properties.setProperty("javax.sip.OUTBOUND_PROXY", outboundProxy + ":" + myself.getPort() + "/" + myself.getProtocol()); } SipStack stack = mSipStack = sipFactory.createSipStack(properties); try { SipProvider provider = stack.createSipProvider( stack.createListeningPoint(localIp, allocateLocalPort(), myself.getProtocol())); provider.addSipListener(this); mSipHelper = new SipHelper(stack, provider); } catch (InvalidArgumentException e) { throw new IOException(e.getMessage()); } catch (TooManyListenersException e) { // must never happen throw new SipException("SipSessionGroup constructor", e); } Log.d(TAG, " start stack for " + myself.getUriString()); stack.start(); mLastNonce = null; mCallReceiverSession = null; mSessionMap.clear(); } synchronized void onConnectivityChanged() { for (SipSessionImpl s : mSessionMap.values()) { s.onError(SipErrorCode.DATA_CONNECTION_LOST, "data connection lost"); } } public SipProfile getLocalProfile() { return mLocalProfile; } public String getLocalProfileUri() { return mLocalProfile.getUriString(); } private String getStackName() { return "stack" + System.currentTimeMillis(); } public synchronized void close() { Log.d(TAG, " close stack for " + mLocalProfile.getUriString()); mSessionMap.clear(); closeToNotReceiveCalls(); if (mSipStack != null) { mSipStack.stop(); mSipStack = null; mSipHelper = null; } } 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)); } 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 key = SipHelper.getCallId(event); Log.d(TAG, "sesssion key from event: " + key); Log.d(TAG, "active sessions:"); for (String k : mSessionMap.keySet()) { Log.d(TAG, " ..." + k + ": " + mSessionMap.get(k)); } SipSessionImpl session = mSessionMap.get(key); return ((session != null) ? session : mCallReceiverSession); } private synchronized void addSipSession(SipSessionImpl newSession) { removeSipSession(newSession); String key = newSession.getCallId(); Log.d(TAG, "+++ add a session with key: '" + key + "'"); mSessionMap.put(key, newSession); for (String k : mSessionMap.keySet()) { Log.d(TAG, " ..... " + 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)) { Log.w(TAG, "session " + session + " is not associated with key '" + key + "'"); mSessionMap.put(key, s); for (Map.Entry entry : mSessionMap.entrySet()) { if (entry.getValue() == s) { key = entry.getKey(); mSessionMap.remove(key); } } } Log.d(TAG, " remove session " + session + " with key '" + key + "'"); for (String k : mSessionMap.keySet()) { Log.d(TAG, " ..... " + k + ": " + mSessionMap.get(k)); } } public void processRequest(RequestEvent event) { process(event); } public void processResponse(ResponseEvent event) { process(event); } public void processIOException(IOExceptionEvent event) { process(event); } public void processTimeout(TimeoutEvent event) { process(event); } public void processTransactionTerminated(TransactionTerminatedEvent event) { process(event); } public void processDialogTerminated(DialogTerminatedEvent event) { process(event); } private synchronized void process(EventObject event) { SipSessionImpl session = getSipSession(event); try { if ((session != null) && session.process(event)) { Log.d(TAG, " ~~~~~ new state: " + session.mState); } else { Log.d(TAG, "event not processed: " + event); } } catch (Throwable e) { Log.w(TAG, "event process error: " + event, 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 class SipSessionCallReceiverImpl extends SipSessionImpl { public SipSessionCallReceiverImpl(ISipSessionListener listener) { super(listener); } public boolean process(EventObject evt) throws SipException { Log.d(TAG, " ~~~~~ " + this + ": " + mState + ": processing " + log(evt)); if (isRequestEvent(Request.INVITE, evt)) { RequestEvent event = (RequestEvent) evt; SipSessionImpl newSession = new SipSessionImpl(mProxy); newSession.mServerTransaction = mSipHelper.sendRinging(event, generateTag()); newSession.mDialog = newSession.mServerTransaction.getDialog(); newSession.mInviteReceived = event; newSession.mPeerProfile = createPeerProfile(event.getRequest()); newSession.mState = SipSessionState.INCOMING_CALL; newSession.mPeerSessionDescription = extractContent(event.getRequest()); addSipSession(newSession); mProxy.onRinging(newSession, newSession.mPeerProfile, newSession.mPeerSessionDescription); return true; } else { return false; } } } class SipSessionImpl extends ISipSession.Stub { SipProfile mPeerProfile; SipSessionListenerProxy mProxy = new SipSessionListenerProxy(); SipSessionState mState = SipSessionState.READY_TO_CALL; RequestEvent mInviteReceived; Dialog mDialog; ServerTransaction mServerTransaction; ClientTransaction mClientTransaction; String mPeerSessionDescription; boolean mInCall; boolean mReRegisterFlag = false; public SipSessionImpl(ISipSessionListener listener) { setListener(listener); } SipSessionImpl duplicate() { return new SipSessionImpl(mProxy.getListener()); } private void reset() { mInCall = false; removeSipSession(this); mPeerProfile = null; mState = SipSessionState.READY_TO_CALL; mInviteReceived = null; mDialog = null; mServerTransaction = null; mClientTransaction = null; mPeerSessionDescription = null; } public boolean isInCall() { return mInCall; } public String getLocalIp() { return mLocalIp; } public SipProfile getLocalProfile() { return mLocalProfile; } public SipProfile getPeerProfile() { return mPeerProfile; } public String getCallId() { return SipHelper.getCallId(getTransaction()); } private Transaction getTransaction() { if (mClientTransaction != null) return mClientTransaction; if (mServerTransaction != null) return mServerTransaction; return null; } public String getState() { return mState.toString(); } 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() { public void run() { try { processCommand(command); } catch (SipException e) { Log.w(TAG, "command error: " + command, e); onError(e); } } }).start(); } public void makeCall(SipProfile peerProfile, String sessionDescription) { doCommandAsync( new MakeCallCommand(peerProfile, sessionDescription)); } public void answerCall(String sessionDescription) { try { processCommand( new MakeCallCommand(mPeerProfile, sessionDescription)); } catch (SipException e) { onError(e); } } public void endCall() { doCommandAsync(END_CALL); } public void changeCall(String sessionDescription) { doCommandAsync( new MakeCallCommand(mPeerProfile, sessionDescription)); } public void register(int duration) { doCommandAsync(new RegisterCommand(duration)); } public void unregister() { doCommandAsync(DEREGISTER); } public boolean isReRegisterRequired() { return mReRegisterFlag; } public void clearReRegisterRequired() { mReRegisterFlag = false; } public void sendKeepAlive() { mState = SipSessionState.PINGING; try { processCommand(new OptionsCommand()); while (SipSessionState.PINGING.equals(mState)) { Thread.sleep(1000); } } catch (SipException e) { Log.e(TAG, "sendKeepAlive failed", e); } catch (InterruptedException e) { Log.e(TAG, "sendKeepAlive interrupted", e); } } private void processCommand(EventObject command) throws SipException { 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)); } public String toString() { try { String s = super.toString(); return s.substring(s.indexOf("@")) + ":" + mState; } catch (Throwable e) { return super.toString(); } } public boolean process(EventObject evt) throws SipException { Log.d(TAG, " ~~~~~ " + this + ": " + mState + ": processing " + log(evt)); synchronized (SipSessionGroup.this) { if (isClosed()) return false; Dialog dialog = null; if (evt instanceof RequestEvent) { dialog = ((RequestEvent) evt).getDialog(); } else if (evt instanceof ResponseEvent) { dialog = ((ResponseEvent) evt).getDialog(); } if (dialog != null) mDialog = dialog; boolean processed; switch (mState) { case REGISTERING: case DEREGISTERING: processed = registeringToReady(evt); break; case PINGING: processed = keepAliveProcess(evt); break; case READY_TO_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; 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 (evt instanceof TimeoutEvent) { processTimeout((TimeoutEvent) evt); } else { processTransactionTerminated( (TransactionTerminatedEvent) evt); } 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 { Log.d(TAG, "not the current dialog; current=" + mDialog + ", terminated=" + event.getDialog()); } } private void processTransactionTerminated( TransactionTerminatedEvent event) { switch (mState) { case IN_CALL: case READY_TO_CALL: Log.d(TAG, "Transaction terminated; do nothing"); break; default: Log.d(TAG, "Transaction terminated early: " + this); onError(SipErrorCode.TRANSACTION_TERMINTED, "transaction terminated"); } } 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) && (mState != SipSessionState.PINGING)) { Log.d(TAG, "not the current transaction; current=" + current + ", timed out=" + target); return; } switch (mState) { case REGISTERING: case DEREGISTERING: reset(); mProxy.onRegistrationTimeout(this); break; case INCOMING_CALL: case INCOMING_CALL_ANSWERING: case OUTGOING_CALL: case OUTGOING_CALL_CANCELING: onError(SipErrorCode.TIME_OUT, event.toString()); break; case PINGING: reset(); mReRegisterFlag = true; mState = SipSessionState.READY_TO_CALL; break; default: Log.d(TAG, " do nothing"); break; } } private int getExpiryTime(Response response) { int expires = EXPIRY_TIME; ExpiresHeader expiresHeader = (ExpiresHeader) response.getHeader(ExpiresHeader.NAME); if (expiresHeader != null) expires = expiresHeader.getExpires(); expiresHeader = (ExpiresHeader) response.getHeader(MinExpiresHeader.NAME); if (expiresHeader != null) { expires = Math.max(expires, expiresHeader.getExpires()); } return expires; } private boolean keepAliveProcess(EventObject evt) throws SipException { if (evt instanceof OptionsCommand) { mClientTransaction = mSipHelper.sendKeepAlive(mLocalProfile, generateTag()); mDialog = mClientTransaction.getDialog(); addSipSession(this); return true; } else if (evt instanceof ResponseEvent) { return parseOptionsResult(evt); } return false; } 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) { mReRegisterFlag = true; Log.w(TAG, String.format("rport is changed: %d <> %d", mRPort, rPort)); mRPort = rPort; } else { Log.w(TAG, "rport is the same: " + rPort); } } else { Log.w(TAG, "peer did not respect our rport request"); } reset(); return true; } return false; } private int getRPortFromResponse(Response response) { ViaHeader viaHeader = (ViaHeader)(response.getHeader( SIPHeaderNames.VIA)); return (viaHeader == null) ? -1 : viaHeader.getRPort(); } 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: SipSessionState state = mState; onRegistrationDone((state == SipSessionState.REGISTERING) ? getExpiryTime(((ResponseEvent) evt).getResponse()) : -1); mLastNonce = null; mRPort = 0; return true; case Response.UNAUTHORIZED: case Response.PROXY_AUTHENTICATION_REQUIRED: if (!handleAuthentication(event)) { Log.v(TAG, "Incorrect username/password"); onRegistrationFailed(SipErrorCode.INVALID_CREDENTIALS, "incorrect username or password"); } 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) && nonce.equals(mLastNonce)) || (nonce == null)) { mLastNonce = nonce; return false; } else { mClientTransaction = mSipHelper.handleChallenge( event, getAccountManager()); mDialog = mClientTransaction.getDialog(); mLastNonce = nonce; return true; } } private AccountManager getAccountManager() { return new AccountManager() { public UserCredentials getCredentials(ClientTransaction challengedTransaction, String realm) { return new UserCredentials() { public String getUserName() { return mLocalProfile.getUserName(); } public String getPassword() { return mPassword; } public String getSipDomain() { return mLocalProfile.getSipDomain(); } }; } }; } 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 boolean readyForCall(EventObject evt) throws SipException { // expect MakeCallCommand, RegisterCommand, DEREGISTER if (evt instanceof MakeCallCommand) { MakeCallCommand cmd = (MakeCallCommand) evt; mPeerProfile = cmd.getPeerProfile(); mClientTransaction = mSipHelper.sendInvite(mLocalProfile, mPeerProfile, cmd.getSessionDescription(), generateTag()); mDialog = mClientTransaction.getDialog(); addSipSession(this); mState = SipSessionState.OUTGOING_CALL; mProxy.onCalling(this); return true; } else if (evt instanceof RegisterCommand) { int duration = ((RegisterCommand) evt).getDuration(); mClientTransaction = mSipHelper.sendRegister(mLocalProfile, generateTag(), duration); mDialog = mClientTransaction.getDialog(); addSipSession(this); mState = SipSessionState.REGISTERING; mProxy.onRegistering(this); return true; } else if (DEREGISTER == evt) { mClientTransaction = mSipHelper.sendRegister(mLocalProfile, generateTag(), 0); mDialog = mClientTransaction.getDialog(); addSipSession(this); mState = SipSessionState.DEREGISTERING; 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 mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived, mLocalProfile, ((MakeCallCommand) evt).getSessionDescription(), mServerTransaction); mState = SipSessionState.INCOMING_CALL_ANSWERING; 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) throws SipException { // expect ACK, CANCEL request if (isRequestEvent(Request.ACK, evt)) { establishCall(); 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: if (mState == SipSessionState.OUTGOING_CALL) { mState = SipSessionState.OUTGOING_CALL_RING_BACK; mProxy.onRingingBack(this); } return true; case Response.OK: mSipHelper.sendInviteAck(event, mDialog); mPeerSessionDescription = extractContent(response); establishCall(); return true; case Response.PROXY_AUTHENTICATION_REQUIRED: if (handleAuthentication(event)) { addSipSession(this); } else { endCallOnError(SipErrorCode.INVALID_CREDENTIALS, "incorrect username or password"); } 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 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. 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.OK) { outgoingCall(evt); // abort Cancel 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 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); endCallNormally(); return true; } else if (isRequestEvent(Request.INVITE, evt)) { // got Re-INVITE RequestEvent event = mInviteReceived = (RequestEvent) evt; mState = SipSessionState.INCOMING_CALL; 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 (evt instanceof MakeCallCommand) { // to change call mClientTransaction = mSipHelper.sendReinvite(mDialog, ((MakeCallCommand) evt).getSessionDescription()); mState = SipSessionState.OUTGOING_CALL; return true; } return false; } private String createErrorMessage(Response response) { return String.format(SERVER_ERROR_PREFIX + "%s (%d)", response.getReasonPhrase(), response.getStatusCode()); } private void establishCall() { mState = SipSessionState.IN_CALL; mInCall = true; mProxy.onCallEstablished(this, mPeerSessionDescription); } private void fallbackToPreviousInCall(Throwable exception) { exception = getRootCause(exception); fallbackToPreviousInCall(getErrorCode(exception), exception.toString()); } private void fallbackToPreviousInCall(SipErrorCode errorCode, String message) { mState = SipSessionState.IN_CALL; mProxy.onCallChangeFailed(this, errorCode.toString(), message); } private void endCallNormally() { reset(); mProxy.onCallEnded(this); } private void endCallOnError(SipErrorCode errorCode, String message) { reset(); mProxy.onError(this, errorCode.toString(), message); } private void endCallOnBusy() { reset(); mProxy.onCallBusy(this); } private void onError(SipErrorCode errorCode, String message) { switch (mState) { case REGISTERING: case DEREGISTERING: onRegistrationFailed(errorCode, message); break; default: if ((errorCode != SipErrorCode.DATA_CONNECTION_LOST) && mInCall) { fallbackToPreviousInCall(errorCode, message); } else { 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.TEMPORARILY_UNAVAILABLE) || (statusCode == Response.BUSY_HERE))) { endCallOnBusy(); } else { onError(getErrorCode(statusCode), createErrorMessage(response)); } } private SipErrorCode getErrorCode(int responseStatusCode) { switch (responseStatusCode) { case Response.NOT_FOUND: case Response.ADDRESS_INCOMPLETE: 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 Throwable getRootCause(Throwable exception) { Throwable cause = exception.getCause(); while (cause != null) { exception = cause; cause = exception.getCause(); } return exception; } private SipErrorCode getErrorCode(Throwable exception) { String message = exception.getMessage(); if (exception instanceof UnknownHostException) { return SipErrorCode.INVALID_REMOTE_URI; } else if (exception instanceof IOException) { return SipErrorCode.SOCKET_ERROR; } else if (message.startsWith(SERVER_ERROR_PREFIX)) { return SipErrorCode.SERVER_ERROR; } else { return SipErrorCode.CLIENT_ERROR; } } private void onRegistrationDone(int duration) { reset(); mProxy.onRegistrationDone(this, duration); } private void onRegistrationFailed(SipErrorCode errorCode, String message) { reset(); mProxy.onRegistrationFailed(this, errorCode.toString(), message); } private void onRegistrationFailed(Throwable exception) { reset(); exception = getRootCause(exception); onRegistrationFailed(getErrorCode(exception), exception.toString()); } private void onRegistrationFailed(Response response) { reset(); int statusCode = response.getStatusCode(); onRegistrationFailed(getErrorCode(statusCode), createErrorMessage(response)); } } /** * @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(); String username = uri.getUser(); if (username == null) username = ANONYMOUS; return new SipProfile.Builder(username, uri.getHost()) .setPort(uri.getPort()) .setDisplayName(address.getDisplayName()) .build(); } catch (IllegalArgumentException e) { throw new SipException("createPeerProfile()", e); } catch (ParseException e) { throw new SipException("createPeerProfile()", e); } } private static 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(); } } private class OptionsCommand extends EventObject { public OptionsCommand() { super(SipSessionGroup.this); } } 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; public MakeCallCommand(SipProfile peerProfile, String sessionDescription) { super(peerProfile); mSessionDescription = sessionDescription; } public SipProfile getPeerProfile() { return (SipProfile) getSource(); } public String getSessionDescription() { return mSessionDescription; } } }