Home | History | Annotate | Download | only in sip
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.sip;
     18 
     19 import gov.nist.javax.sip.clientauthutils.AccountManager;
     20 import gov.nist.javax.sip.clientauthutils.UserCredentials;
     21 import gov.nist.javax.sip.header.ProxyAuthenticate;
     22 import gov.nist.javax.sip.header.ReferTo;
     23 import gov.nist.javax.sip.header.SIPHeaderNames;
     24 import gov.nist.javax.sip.header.StatusLine;
     25 import gov.nist.javax.sip.header.WWWAuthenticate;
     26 import gov.nist.javax.sip.header.extensions.ReferredByHeader;
     27 import gov.nist.javax.sip.header.extensions.ReplacesHeader;
     28 import gov.nist.javax.sip.message.SIPMessage;
     29 import gov.nist.javax.sip.message.SIPResponse;
     30 
     31 import android.net.sip.ISipSession;
     32 import android.net.sip.ISipSessionListener;
     33 import android.net.sip.SipErrorCode;
     34 import android.net.sip.SipProfile;
     35 import android.net.sip.SipSession;
     36 import android.net.sip.SipSessionAdapter;
     37 import android.text.TextUtils;
     38 import android.util.Log;
     39 
     40 import java.io.IOException;
     41 import java.io.UnsupportedEncodingException;
     42 import java.net.DatagramSocket;
     43 import java.net.UnknownHostException;
     44 import java.text.ParseException;
     45 import java.util.Collection;
     46 import java.util.EventObject;
     47 import java.util.HashMap;
     48 import java.util.Map;
     49 import java.util.Properties;
     50 import java.util.TooManyListenersException;
     51 
     52 import javax.sip.ClientTransaction;
     53 import javax.sip.Dialog;
     54 import javax.sip.DialogTerminatedEvent;
     55 import javax.sip.IOExceptionEvent;
     56 import javax.sip.InvalidArgumentException;
     57 import javax.sip.ListeningPoint;
     58 import javax.sip.ObjectInUseException;
     59 import javax.sip.RequestEvent;
     60 import javax.sip.ResponseEvent;
     61 import javax.sip.ServerTransaction;
     62 import javax.sip.SipException;
     63 import javax.sip.SipFactory;
     64 import javax.sip.SipListener;
     65 import javax.sip.SipProvider;
     66 import javax.sip.SipStack;
     67 import javax.sip.TimeoutEvent;
     68 import javax.sip.Transaction;
     69 import javax.sip.TransactionState;
     70 import javax.sip.TransactionTerminatedEvent;
     71 import javax.sip.TransactionUnavailableException;
     72 import javax.sip.address.Address;
     73 import javax.sip.address.SipURI;
     74 import javax.sip.header.CSeqHeader;
     75 import javax.sip.header.ContactHeader;
     76 import javax.sip.header.ExpiresHeader;
     77 import javax.sip.header.FromHeader;
     78 import javax.sip.header.HeaderAddress;
     79 import javax.sip.header.MinExpiresHeader;
     80 import javax.sip.header.ReferToHeader;
     81 import javax.sip.header.ViaHeader;
     82 import javax.sip.message.Message;
     83 import javax.sip.message.Request;
     84 import javax.sip.message.Response;
     85 
     86 
     87 /**
     88  * Manages {@link ISipSession}'s for a SIP account.
     89  */
     90 class SipSessionGroup implements SipListener {
     91     private static final String TAG = "SipSession";
     92     private static final boolean DEBUG = false;
     93     private static final boolean DEBUG_PING = false;
     94     private static final String ANONYMOUS = "anonymous";
     95     // Limit the size of thread pool to 1 for the order issue when the phone is
     96     // waken up from sleep and there are many packets to be processed in the SIP
     97     // stack. Note: The default thread pool size in NIST SIP stack is -1 which is
     98     // unlimited.
     99     private static final String THREAD_POOL_SIZE = "1";
    100     private static final int EXPIRY_TIME = 3600; // in seconds
    101     private static final int CANCEL_CALL_TIMER = 3; // in seconds
    102     private static final int END_CALL_TIMER = 3; // in seconds
    103     private static final int KEEPALIVE_TIMEOUT = 5; // in seconds
    104     private static final int INCALL_KEEPALIVE_INTERVAL = 10; // in seconds
    105     private static final long WAKE_LOCK_HOLDING_TIME = 500; // in milliseconds
    106 
    107     private static final EventObject DEREGISTER = new EventObject("Deregister");
    108     private static final EventObject END_CALL = new EventObject("End call");
    109     private static final EventObject HOLD_CALL = new EventObject("Hold call");
    110     private static final EventObject CONTINUE_CALL
    111             = new EventObject("Continue call");
    112 
    113     private final SipProfile mLocalProfile;
    114     private final String mPassword;
    115 
    116     private SipStack mSipStack;
    117     private SipHelper mSipHelper;
    118 
    119     // session that processes INVITE requests
    120     private SipSessionImpl mCallReceiverSession;
    121     private String mLocalIp;
    122 
    123     private SipWakeupTimer mWakeupTimer;
    124     private SipWakeLock mWakeLock;
    125 
    126     // call-id-to-SipSession map
    127     private Map<String, SipSessionImpl> mSessionMap =
    128             new HashMap<String, SipSessionImpl>();
    129 
    130     // external address observed from any response
    131     private String mExternalIp;
    132     private int mExternalPort;
    133 
    134     /**
    135      * @param myself the local profile with password crossed out
    136      * @param password the password of the profile
    137      * @throws IOException if cannot assign requested address
    138      */
    139     public SipSessionGroup(String localIp, SipProfile myself, String password,
    140             SipWakeupTimer timer, SipWakeLock wakeLock) throws SipException,
    141             IOException {
    142         mLocalProfile = myself;
    143         mPassword = password;
    144         mWakeupTimer = timer;
    145         mWakeLock = wakeLock;
    146         reset(localIp);
    147     }
    148 
    149     // TODO: remove this method once SipWakeupTimer can better handle variety
    150     // of timeout values
    151     void setWakeupTimer(SipWakeupTimer timer) {
    152         mWakeupTimer = timer;
    153     }
    154 
    155     synchronized void reset(String localIp) throws SipException, IOException {
    156         mLocalIp = localIp;
    157         if (localIp == null) return;
    158 
    159         SipProfile myself = mLocalProfile;
    160         SipFactory sipFactory = SipFactory.getInstance();
    161         Properties properties = new Properties();
    162         properties.setProperty("javax.sip.STACK_NAME", getStackName());
    163         properties.setProperty(
    164                 "gov.nist.javax.sip.THREAD_POOL_SIZE", THREAD_POOL_SIZE);
    165         String outboundProxy = myself.getProxyAddress();
    166         if (!TextUtils.isEmpty(outboundProxy)) {
    167             Log.v(TAG, "outboundProxy is " + outboundProxy);
    168             properties.setProperty("javax.sip.OUTBOUND_PROXY", outboundProxy
    169                     + ":" + myself.getPort() + "/" + myself.getProtocol());
    170         }
    171         SipStack stack = mSipStack = sipFactory.createSipStack(properties);
    172 
    173         try {
    174             SipProvider provider = stack.createSipProvider(
    175                     stack.createListeningPoint(localIp, allocateLocalPort(),
    176                             myself.getProtocol()));
    177             provider.addSipListener(this);
    178             mSipHelper = new SipHelper(stack, provider);
    179         } catch (InvalidArgumentException e) {
    180             throw new IOException(e.getMessage());
    181         } catch (TooManyListenersException e) {
    182             // must never happen
    183             throw new SipException("SipSessionGroup constructor", e);
    184         }
    185         Log.d(TAG, " start stack for " + myself.getUriString());
    186         stack.start();
    187 
    188         mCallReceiverSession = null;
    189         mSessionMap.clear();
    190 
    191         resetExternalAddress();
    192     }
    193 
    194     synchronized void onConnectivityChanged() {
    195         SipSessionImpl[] ss = mSessionMap.values().toArray(
    196                     new SipSessionImpl[mSessionMap.size()]);
    197         // Iterate on the copied array instead of directly on mSessionMap to
    198         // avoid ConcurrentModificationException being thrown when
    199         // SipSessionImpl removes itself from mSessionMap in onError() in the
    200         // following loop.
    201         for (SipSessionImpl s : ss) {
    202             s.onError(SipErrorCode.DATA_CONNECTION_LOST,
    203                     "data connection lost");
    204         }
    205     }
    206 
    207     synchronized void resetExternalAddress() {
    208         if (DEBUG) {
    209             Log.d(TAG, " reset external addr on " + mSipStack);
    210         }
    211         mExternalIp = null;
    212         mExternalPort = 0;
    213     }
    214 
    215     public SipProfile getLocalProfile() {
    216         return mLocalProfile;
    217     }
    218 
    219     public String getLocalProfileUri() {
    220         return mLocalProfile.getUriString();
    221     }
    222 
    223     private String getStackName() {
    224         return "stack" + System.currentTimeMillis();
    225     }
    226 
    227     public synchronized void close() {
    228         Log.d(TAG, " close stack for " + mLocalProfile.getUriString());
    229         onConnectivityChanged();
    230         mSessionMap.clear();
    231         closeToNotReceiveCalls();
    232         if (mSipStack != null) {
    233             mSipStack.stop();
    234             mSipStack = null;
    235             mSipHelper = null;
    236         }
    237     }
    238 
    239     public synchronized boolean isClosed() {
    240         return (mSipStack == null);
    241     }
    242 
    243     // For internal use, require listener not to block in callbacks.
    244     public synchronized void openToReceiveCalls(ISipSessionListener listener) {
    245         if (mCallReceiverSession == null) {
    246             mCallReceiverSession = new SipSessionCallReceiverImpl(listener);
    247         } else {
    248             mCallReceiverSession.setListener(listener);
    249         }
    250     }
    251 
    252     public synchronized void closeToNotReceiveCalls() {
    253         mCallReceiverSession = null;
    254     }
    255 
    256     public ISipSession createSession(ISipSessionListener listener) {
    257         return (isClosed() ? null : new SipSessionImpl(listener));
    258     }
    259 
    260     private static int allocateLocalPort() throws SipException {
    261         try {
    262             DatagramSocket s = new DatagramSocket();
    263             int localPort = s.getLocalPort();
    264             s.close();
    265             return localPort;
    266         } catch (IOException e) {
    267             throw new SipException("allocateLocalPort()", e);
    268         }
    269     }
    270 
    271     synchronized boolean containsSession(String callId) {
    272         return mSessionMap.containsKey(callId);
    273     }
    274 
    275     private synchronized SipSessionImpl getSipSession(EventObject event) {
    276         String key = SipHelper.getCallId(event);
    277         SipSessionImpl session = mSessionMap.get(key);
    278         if ((session != null) && isLoggable(session)) {
    279             Log.d(TAG, "session key from event: " + key);
    280             Log.d(TAG, "active sessions:");
    281             for (String k : mSessionMap.keySet()) {
    282                 Log.d(TAG, " ..." + k + ": " + mSessionMap.get(k));
    283             }
    284         }
    285         return ((session != null) ? session : mCallReceiverSession);
    286     }
    287 
    288     private synchronized void addSipSession(SipSessionImpl newSession) {
    289         removeSipSession(newSession);
    290         String key = newSession.getCallId();
    291         mSessionMap.put(key, newSession);
    292         if (isLoggable(newSession)) {
    293             Log.d(TAG, "+++  add a session with key:  '" + key + "'");
    294             for (String k : mSessionMap.keySet()) {
    295                 Log.d(TAG, "  " + k + ": " + mSessionMap.get(k));
    296             }
    297         }
    298     }
    299 
    300     private synchronized void removeSipSession(SipSessionImpl session) {
    301         if (session == mCallReceiverSession) return;
    302         String key = session.getCallId();
    303         SipSessionImpl s = mSessionMap.remove(key);
    304         // sanity check
    305         if ((s != null) && (s != session)) {
    306             Log.w(TAG, "session " + session + " is not associated with key '"
    307                     + key + "'");
    308             mSessionMap.put(key, s);
    309             for (Map.Entry<String, SipSessionImpl> entry
    310                     : mSessionMap.entrySet()) {
    311                 if (entry.getValue() == s) {
    312                     key = entry.getKey();
    313                     mSessionMap.remove(key);
    314                 }
    315             }
    316         }
    317 
    318         if ((s != null) && isLoggable(s)) {
    319             Log.d(TAG, "remove session " + session + " @key '" + key + "'");
    320             for (String k : mSessionMap.keySet()) {
    321                 Log.d(TAG, "  " + k + ": " + mSessionMap.get(k));
    322             }
    323         }
    324     }
    325 
    326     public void processRequest(final RequestEvent event) {
    327         if (isRequestEvent(Request.INVITE, event)) {
    328             if (DEBUG) Log.d(TAG, "<<<<< got INVITE, thread:"
    329                     + Thread.currentThread());
    330             // Acquire a wake lock and keep it for WAKE_LOCK_HOLDING_TIME;
    331             // should be large enough to bring up the app.
    332             mWakeLock.acquire(WAKE_LOCK_HOLDING_TIME);
    333         }
    334         process(event);
    335     }
    336 
    337     public void processResponse(ResponseEvent event) {
    338         process(event);
    339     }
    340 
    341     public void processIOException(IOExceptionEvent event) {
    342         process(event);
    343     }
    344 
    345     public void processTimeout(TimeoutEvent event) {
    346         process(event);
    347     }
    348 
    349     public void processTransactionTerminated(TransactionTerminatedEvent event) {
    350         process(event);
    351     }
    352 
    353     public void processDialogTerminated(DialogTerminatedEvent event) {
    354         process(event);
    355     }
    356 
    357     private synchronized void process(EventObject event) {
    358         SipSessionImpl session = getSipSession(event);
    359         try {
    360             boolean isLoggable = isLoggable(session, event);
    361             boolean processed = (session != null) && session.process(event);
    362             if (isLoggable && processed) {
    363                 Log.d(TAG, "new state after: "
    364                         + SipSession.State.toString(session.mState));
    365             }
    366         } catch (Throwable e) {
    367             Log.w(TAG, "event process error: " + event, getRootCause(e));
    368             session.onError(e);
    369         }
    370     }
    371 
    372     private String extractContent(Message message) {
    373         // Currently we do not support secure MIME bodies.
    374         byte[] bytes = message.getRawContent();
    375         if (bytes != null) {
    376             try {
    377                 if (message instanceof SIPMessage) {
    378                     return ((SIPMessage) message).getMessageContent();
    379                 } else {
    380                     return new String(bytes, "UTF-8");
    381                 }
    382             } catch (UnsupportedEncodingException e) {
    383             }
    384         }
    385         return null;
    386     }
    387 
    388     private void extractExternalAddress(ResponseEvent evt) {
    389         Response response = evt.getResponse();
    390         ViaHeader viaHeader = (ViaHeader)(response.getHeader(
    391                 SIPHeaderNames.VIA));
    392         if (viaHeader == null) return;
    393         int rport = viaHeader.getRPort();
    394         String externalIp = viaHeader.getReceived();
    395         if ((rport > 0) && (externalIp != null)) {
    396             mExternalIp = externalIp;
    397             mExternalPort = rport;
    398             if (DEBUG) {
    399                 Log.d(TAG, " got external addr " + externalIp + ":" + rport
    400                         + " on " + mSipStack);
    401             }
    402         }
    403     }
    404 
    405     private Throwable getRootCause(Throwable exception) {
    406         Throwable cause = exception.getCause();
    407         while (cause != null) {
    408             exception = cause;
    409             cause = exception.getCause();
    410         }
    411         return exception;
    412     }
    413 
    414     private SipSessionImpl createNewSession(RequestEvent event,
    415             ISipSessionListener listener, ServerTransaction transaction,
    416             int newState) throws SipException {
    417         SipSessionImpl newSession = new SipSessionImpl(listener);
    418         newSession.mServerTransaction = transaction;
    419         newSession.mState = newState;
    420         newSession.mDialog = newSession.mServerTransaction.getDialog();
    421         newSession.mInviteReceived = event;
    422         newSession.mPeerProfile = createPeerProfile((HeaderAddress)
    423                 event.getRequest().getHeader(FromHeader.NAME));
    424         newSession.mPeerSessionDescription =
    425                 extractContent(event.getRequest());
    426         return newSession;
    427     }
    428 
    429     private class SipSessionCallReceiverImpl extends SipSessionImpl {
    430         public SipSessionCallReceiverImpl(ISipSessionListener listener) {
    431             super(listener);
    432         }
    433 
    434         private int processInviteWithReplaces(RequestEvent event,
    435                 ReplacesHeader replaces) {
    436             String callId = replaces.getCallId();
    437             SipSessionImpl session = mSessionMap.get(callId);
    438             if (session == null) {
    439                 return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
    440             }
    441 
    442             Dialog dialog = session.mDialog;
    443             if (dialog == null) return Response.DECLINE;
    444 
    445             if (!dialog.getLocalTag().equals(replaces.getToTag()) ||
    446                     !dialog.getRemoteTag().equals(replaces.getFromTag())) {
    447                 // No match is found, returns 481.
    448                 return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
    449             }
    450 
    451             ReferredByHeader referredBy = (ReferredByHeader) event.getRequest()
    452                     .getHeader(ReferredByHeader.NAME);
    453             if ((referredBy == null) ||
    454                     !dialog.getRemoteParty().equals(referredBy.getAddress())) {
    455                 return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
    456             }
    457             return Response.OK;
    458         }
    459 
    460         private void processNewInviteRequest(RequestEvent event)
    461                 throws SipException {
    462             ReplacesHeader replaces = (ReplacesHeader) event.getRequest()
    463                     .getHeader(ReplacesHeader.NAME);
    464             SipSessionImpl newSession = null;
    465             if (replaces != null) {
    466                 int response = processInviteWithReplaces(event, replaces);
    467                 if (DEBUG) {
    468                     Log.v(TAG, "ReplacesHeader: " + replaces
    469                             + " response=" + response);
    470                 }
    471                 if (response == Response.OK) {
    472                     SipSessionImpl replacedSession =
    473                             mSessionMap.get(replaces.getCallId());
    474                     // got INVITE w/ replaces request.
    475                     newSession = createNewSession(event,
    476                             replacedSession.mProxy.getListener(),
    477                             mSipHelper.getServerTransaction(event),
    478                             SipSession.State.INCOMING_CALL);
    479                     newSession.mProxy.onCallTransferring(newSession,
    480                             newSession.mPeerSessionDescription);
    481                 } else {
    482                     mSipHelper.sendResponse(event, response);
    483                 }
    484             } else {
    485                 // New Incoming call.
    486                 newSession = createNewSession(event, mProxy,
    487                         mSipHelper.sendRinging(event, generateTag()),
    488                         SipSession.State.INCOMING_CALL);
    489                 mProxy.onRinging(newSession, newSession.mPeerProfile,
    490                         newSession.mPeerSessionDescription);
    491             }
    492             if (newSession != null) addSipSession(newSession);
    493         }
    494 
    495         public boolean process(EventObject evt) throws SipException {
    496             if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~   " + this + ": "
    497                     + SipSession.State.toString(mState) + ": processing "
    498                     + log(evt));
    499             if (isRequestEvent(Request.INVITE, evt)) {
    500                 processNewInviteRequest((RequestEvent) evt);
    501                 return true;
    502             } else if (isRequestEvent(Request.OPTIONS, evt)) {
    503                 mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
    504                 return true;
    505             } else {
    506                 return false;
    507             }
    508         }
    509     }
    510 
    511     static interface KeepAliveProcessCallback {
    512         /** Invoked when the response of keeping alive comes back. */
    513         void onResponse(boolean portChanged);
    514         void onError(int errorCode, String description);
    515     }
    516 
    517     class SipSessionImpl extends ISipSession.Stub {
    518         SipProfile mPeerProfile;
    519         SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
    520         int mState = SipSession.State.READY_TO_CALL;
    521         RequestEvent mInviteReceived;
    522         Dialog mDialog;
    523         ServerTransaction mServerTransaction;
    524         ClientTransaction mClientTransaction;
    525         String mPeerSessionDescription;
    526         boolean mInCall;
    527         SessionTimer mSessionTimer;
    528         int mAuthenticationRetryCount;
    529 
    530         private KeepAliveProcess mKeepAliveProcess;
    531 
    532         private SipSessionImpl mKeepAliveSession;
    533 
    534         // the following three members are used for handling refer request.
    535         SipSessionImpl mReferSession;
    536         ReferredByHeader mReferredBy;
    537         String mReplaces;
    538 
    539         // lightweight timer
    540         class SessionTimer {
    541             private boolean mRunning = true;
    542 
    543             void start(final int timeout) {
    544                 new Thread(new Runnable() {
    545                     public void run() {
    546                         sleep(timeout);
    547                         if (mRunning) timeout();
    548                     }
    549                 }, "SipSessionTimerThread").start();
    550             }
    551 
    552             synchronized void cancel() {
    553                 mRunning = false;
    554                 this.notify();
    555             }
    556 
    557             private void timeout() {
    558                 synchronized (SipSessionGroup.this) {
    559                     onError(SipErrorCode.TIME_OUT, "Session timed out!");
    560                 }
    561             }
    562 
    563             private synchronized void sleep(int timeout) {
    564                 try {
    565                     this.wait(timeout * 1000);
    566                 } catch (InterruptedException e) {
    567                     Log.e(TAG, "session timer interrupted!");
    568                 }
    569             }
    570         }
    571 
    572         public SipSessionImpl(ISipSessionListener listener) {
    573             setListener(listener);
    574         }
    575 
    576         SipSessionImpl duplicate() {
    577             return new SipSessionImpl(mProxy.getListener());
    578         }
    579 
    580         private void reset() {
    581             mInCall = false;
    582             removeSipSession(this);
    583             mPeerProfile = null;
    584             mState = SipSession.State.READY_TO_CALL;
    585             mInviteReceived = null;
    586             mPeerSessionDescription = null;
    587             mAuthenticationRetryCount = 0;
    588             mReferSession = null;
    589             mReferredBy = null;
    590             mReplaces = null;
    591 
    592             if (mDialog != null) mDialog.delete();
    593             mDialog = null;
    594 
    595             try {
    596                 if (mServerTransaction != null) mServerTransaction.terminate();
    597             } catch (ObjectInUseException e) {
    598                 // ignored
    599             }
    600             mServerTransaction = null;
    601 
    602             try {
    603                 if (mClientTransaction != null) mClientTransaction.terminate();
    604             } catch (ObjectInUseException e) {
    605                 // ignored
    606             }
    607             mClientTransaction = null;
    608 
    609             cancelSessionTimer();
    610 
    611             if (mKeepAliveSession != null) {
    612                 mKeepAliveSession.stopKeepAliveProcess();
    613                 mKeepAliveSession = null;
    614             }
    615         }
    616 
    617         public boolean isInCall() {
    618             return mInCall;
    619         }
    620 
    621         public String getLocalIp() {
    622             return mLocalIp;
    623         }
    624 
    625         public SipProfile getLocalProfile() {
    626             return mLocalProfile;
    627         }
    628 
    629         public SipProfile getPeerProfile() {
    630             return mPeerProfile;
    631         }
    632 
    633         public String getCallId() {
    634             return SipHelper.getCallId(getTransaction());
    635         }
    636 
    637         private Transaction getTransaction() {
    638             if (mClientTransaction != null) return mClientTransaction;
    639             if (mServerTransaction != null) return mServerTransaction;
    640             return null;
    641         }
    642 
    643         public int getState() {
    644             return mState;
    645         }
    646 
    647         public void setListener(ISipSessionListener listener) {
    648             mProxy.setListener((listener instanceof SipSessionListenerProxy)
    649                     ? ((SipSessionListenerProxy) listener).getListener()
    650                     : listener);
    651         }
    652 
    653         // process the command in a new thread
    654         private void doCommandAsync(final EventObject command) {
    655             new Thread(new Runnable() {
    656                     public void run() {
    657                         try {
    658                             processCommand(command);
    659                         } catch (Throwable e) {
    660                             Log.w(TAG, "command error: " + command + ": "
    661                                     + mLocalProfile.getUriString(),
    662                                     getRootCause(e));
    663                             onError(e);
    664                         }
    665                     }
    666             }, "SipSessionAsyncCmdThread").start();
    667         }
    668 
    669         public void makeCall(SipProfile peerProfile, String sessionDescription,
    670                 int timeout) {
    671             doCommandAsync(new MakeCallCommand(peerProfile, sessionDescription,
    672                     timeout));
    673         }
    674 
    675         public void answerCall(String sessionDescription, int timeout) {
    676             synchronized (SipSessionGroup.this) {
    677                 if (mPeerProfile == null) return;
    678                 doCommandAsync(new MakeCallCommand(mPeerProfile,
    679                         sessionDescription, timeout));
    680             }
    681         }
    682 
    683         public void endCall() {
    684             doCommandAsync(END_CALL);
    685         }
    686 
    687         public void changeCall(String sessionDescription, int timeout) {
    688             synchronized (SipSessionGroup.this) {
    689                 if (mPeerProfile == null) return;
    690                 doCommandAsync(new MakeCallCommand(mPeerProfile,
    691                         sessionDescription, timeout));
    692             }
    693         }
    694 
    695         public void register(int duration) {
    696             doCommandAsync(new RegisterCommand(duration));
    697         }
    698 
    699         public void unregister() {
    700             doCommandAsync(DEREGISTER);
    701         }
    702 
    703         private void processCommand(EventObject command) throws SipException {
    704             if (isLoggable(command)) Log.d(TAG, "process cmd: " + command);
    705             if (!process(command)) {
    706                 onError(SipErrorCode.IN_PROGRESS,
    707                         "cannot initiate a new transaction to execute: "
    708                         + command);
    709             }
    710         }
    711 
    712         protected String generateTag() {
    713             // 32-bit randomness
    714             return String.valueOf((long) (Math.random() * 0x100000000L));
    715         }
    716 
    717         public String toString() {
    718             try {
    719                 String s = super.toString();
    720                 return s.substring(s.indexOf("@")) + ":"
    721                         + SipSession.State.toString(mState);
    722             } catch (Throwable e) {
    723                 return super.toString();
    724             }
    725         }
    726 
    727         public boolean process(EventObject evt) throws SipException {
    728             if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~   " + this + ": "
    729                     + SipSession.State.toString(mState) + ": processing "
    730                     + log(evt));
    731             synchronized (SipSessionGroup.this) {
    732                 if (isClosed()) return false;
    733 
    734                 if (mKeepAliveProcess != null) {
    735                     // event consumed by keepalive process
    736                     if (mKeepAliveProcess.process(evt)) return true;
    737                 }
    738 
    739                 Dialog dialog = null;
    740                 if (evt instanceof RequestEvent) {
    741                     dialog = ((RequestEvent) evt).getDialog();
    742                 } else if (evt instanceof ResponseEvent) {
    743                     dialog = ((ResponseEvent) evt).getDialog();
    744                     extractExternalAddress((ResponseEvent) evt);
    745                 }
    746                 if (dialog != null) mDialog = dialog;
    747 
    748                 boolean processed;
    749 
    750                 switch (mState) {
    751                 case SipSession.State.REGISTERING:
    752                 case SipSession.State.DEREGISTERING:
    753                     processed = registeringToReady(evt);
    754                     break;
    755                 case SipSession.State.READY_TO_CALL:
    756                     processed = readyForCall(evt);
    757                     break;
    758                 case SipSession.State.INCOMING_CALL:
    759                     processed = incomingCall(evt);
    760                     break;
    761                 case SipSession.State.INCOMING_CALL_ANSWERING:
    762                     processed = incomingCallToInCall(evt);
    763                     break;
    764                 case SipSession.State.OUTGOING_CALL:
    765                 case SipSession.State.OUTGOING_CALL_RING_BACK:
    766                     processed = outgoingCall(evt);
    767                     break;
    768                 case SipSession.State.OUTGOING_CALL_CANCELING:
    769                     processed = outgoingCallToReady(evt);
    770                     break;
    771                 case SipSession.State.IN_CALL:
    772                     processed = inCall(evt);
    773                     break;
    774                 case SipSession.State.ENDING_CALL:
    775                     processed = endingCall(evt);
    776                     break;
    777                 default:
    778                     processed = false;
    779                 }
    780                 return (processed || processExceptions(evt));
    781             }
    782         }
    783 
    784         private boolean processExceptions(EventObject evt) throws SipException {
    785             if (isRequestEvent(Request.BYE, evt)) {
    786                 // terminate the call whenever a BYE is received
    787                 mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
    788                 endCallNormally();
    789                 return true;
    790             } else if (isRequestEvent(Request.CANCEL, evt)) {
    791                 mSipHelper.sendResponse((RequestEvent) evt,
    792                         Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST);
    793                 return true;
    794             } else if (evt instanceof TransactionTerminatedEvent) {
    795                 if (isCurrentTransaction((TransactionTerminatedEvent) evt)) {
    796                     if (evt instanceof TimeoutEvent) {
    797                         processTimeout((TimeoutEvent) evt);
    798                     } else {
    799                         processTransactionTerminated(
    800                                 (TransactionTerminatedEvent) evt);
    801                     }
    802                     return true;
    803                 }
    804             } else if (isRequestEvent(Request.OPTIONS, evt)) {
    805                 mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
    806                 return true;
    807             } else if (evt instanceof DialogTerminatedEvent) {
    808                 processDialogTerminated((DialogTerminatedEvent) evt);
    809                 return true;
    810             }
    811             return false;
    812         }
    813 
    814         private void processDialogTerminated(DialogTerminatedEvent event) {
    815             if (mDialog == event.getDialog()) {
    816                 onError(new SipException("dialog terminated"));
    817             } else {
    818                 Log.d(TAG, "not the current dialog; current=" + mDialog
    819                         + ", terminated=" + event.getDialog());
    820             }
    821         }
    822 
    823         private boolean isCurrentTransaction(TransactionTerminatedEvent event) {
    824             Transaction current = event.isServerTransaction()
    825                     ? mServerTransaction
    826                     : mClientTransaction;
    827             Transaction target = event.isServerTransaction()
    828                     ? event.getServerTransaction()
    829                     : event.getClientTransaction();
    830 
    831             if ((current != target) && (mState != SipSession.State.PINGING)) {
    832                 Log.d(TAG, "not the current transaction; current="
    833                         + toString(current) + ", target=" + toString(target));
    834                 return false;
    835             } else if (current != null) {
    836                 Log.d(TAG, "transaction terminated: " + toString(current));
    837                 return true;
    838             } else {
    839                 // no transaction; shouldn't be here; ignored
    840                 return true;
    841             }
    842         }
    843 
    844         private String toString(Transaction transaction) {
    845             if (transaction == null) return "null";
    846             Request request = transaction.getRequest();
    847             Dialog dialog = transaction.getDialog();
    848             CSeqHeader cseq = (CSeqHeader) request.getHeader(CSeqHeader.NAME);
    849             return String.format("req=%s,%s,s=%s,ds=%s,", request.getMethod(),
    850                     cseq.getSeqNumber(), transaction.getState(),
    851                     ((dialog == null) ? "-" : dialog.getState()));
    852         }
    853 
    854         private void processTransactionTerminated(
    855                 TransactionTerminatedEvent event) {
    856             switch (mState) {
    857                 case SipSession.State.IN_CALL:
    858                 case SipSession.State.READY_TO_CALL:
    859                     Log.d(TAG, "Transaction terminated; do nothing");
    860                     break;
    861                 default:
    862                     Log.d(TAG, "Transaction terminated early: " + this);
    863                     onError(SipErrorCode.TRANSACTION_TERMINTED,
    864                             "transaction terminated");
    865             }
    866         }
    867 
    868         private void processTimeout(TimeoutEvent event) {
    869             Log.d(TAG, "processing Timeout...");
    870             switch (mState) {
    871                 case SipSession.State.REGISTERING:
    872                 case SipSession.State.DEREGISTERING:
    873                     reset();
    874                     mProxy.onRegistrationTimeout(this);
    875                     break;
    876                 case SipSession.State.INCOMING_CALL:
    877                 case SipSession.State.INCOMING_CALL_ANSWERING:
    878                 case SipSession.State.OUTGOING_CALL:
    879                 case SipSession.State.OUTGOING_CALL_CANCELING:
    880                     onError(SipErrorCode.TIME_OUT, event.toString());
    881                     break;
    882 
    883                 default:
    884                     Log.d(TAG, "   do nothing");
    885                     break;
    886             }
    887         }
    888 
    889         private int getExpiryTime(Response response) {
    890             int time = -1;
    891             ContactHeader contact = (ContactHeader) response.getHeader(ContactHeader.NAME);
    892             if (contact != null) {
    893                 time = contact.getExpires();
    894             }
    895             ExpiresHeader expires = (ExpiresHeader) response.getHeader(ExpiresHeader.NAME);
    896             if (expires != null && (time < 0 || time > expires.getExpires())) {
    897                 time = expires.getExpires();
    898             }
    899             if (time <= 0) {
    900                 time = EXPIRY_TIME;
    901             }
    902             expires = (ExpiresHeader) response.getHeader(MinExpiresHeader.NAME);
    903             if (expires != null && time < expires.getExpires()) {
    904                 time = expires.getExpires();
    905             }
    906             if (DEBUG) {
    907                 Log.v(TAG, "Expiry time = " + time);
    908             }
    909             return time;
    910         }
    911 
    912         private boolean registeringToReady(EventObject evt)
    913                 throws SipException {
    914             if (expectResponse(Request.REGISTER, evt)) {
    915                 ResponseEvent event = (ResponseEvent) evt;
    916                 Response response = event.getResponse();
    917 
    918                 int statusCode = response.getStatusCode();
    919                 switch (statusCode) {
    920                 case Response.OK:
    921                     int state = mState;
    922                     onRegistrationDone((state == SipSession.State.REGISTERING)
    923                             ? getExpiryTime(((ResponseEvent) evt).getResponse())
    924                             : -1);
    925                     return true;
    926                 case Response.UNAUTHORIZED:
    927                 case Response.PROXY_AUTHENTICATION_REQUIRED:
    928                     handleAuthentication(event);
    929                     return true;
    930                 default:
    931                     if (statusCode >= 500) {
    932                         onRegistrationFailed(response);
    933                         return true;
    934                     }
    935                 }
    936             }
    937             return false;
    938         }
    939 
    940         private boolean handleAuthentication(ResponseEvent event)
    941                 throws SipException {
    942             Response response = event.getResponse();
    943             String nonce = getNonceFromResponse(response);
    944             if (nonce == null) {
    945                 onError(SipErrorCode.SERVER_ERROR,
    946                         "server does not provide challenge");
    947                 return false;
    948             } else if (mAuthenticationRetryCount < 2) {
    949                 mClientTransaction = mSipHelper.handleChallenge(
    950                         event, getAccountManager());
    951                 mDialog = mClientTransaction.getDialog();
    952                 mAuthenticationRetryCount++;
    953                 if (isLoggable(this, event)) {
    954                     Log.d(TAG, "   authentication retry count="
    955                             + mAuthenticationRetryCount);
    956                 }
    957                 return true;
    958             } else {
    959                 if (crossDomainAuthenticationRequired(response)) {
    960                     onError(SipErrorCode.CROSS_DOMAIN_AUTHENTICATION,
    961                             getRealmFromResponse(response));
    962                 } else {
    963                     onError(SipErrorCode.INVALID_CREDENTIALS,
    964                             "incorrect username or password");
    965                 }
    966                 return false;
    967             }
    968         }
    969 
    970         private boolean crossDomainAuthenticationRequired(Response response) {
    971             String realm = getRealmFromResponse(response);
    972             if (realm == null) realm = "";
    973             return !mLocalProfile.getSipDomain().trim().equals(realm.trim());
    974         }
    975 
    976         private AccountManager getAccountManager() {
    977             return new AccountManager() {
    978                 public UserCredentials getCredentials(ClientTransaction
    979                         challengedTransaction, String realm) {
    980                     return new UserCredentials() {
    981                         public String getUserName() {
    982                             String username = mLocalProfile.getAuthUserName();
    983                             return (!TextUtils.isEmpty(username) ? username :
    984                                     mLocalProfile.getUserName());
    985                         }
    986 
    987                         public String getPassword() {
    988                             return mPassword;
    989                         }
    990 
    991                         public String getSipDomain() {
    992                             return mLocalProfile.getSipDomain();
    993                         }
    994                     };
    995                 }
    996             };
    997         }
    998 
    999         private String getRealmFromResponse(Response response) {
   1000             WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
   1001                     SIPHeaderNames.WWW_AUTHENTICATE);
   1002             if (wwwAuth != null) return wwwAuth.getRealm();
   1003             ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
   1004                     SIPHeaderNames.PROXY_AUTHENTICATE);
   1005             return (proxyAuth == null) ? null : proxyAuth.getRealm();
   1006         }
   1007 
   1008         private String getNonceFromResponse(Response response) {
   1009             WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
   1010                     SIPHeaderNames.WWW_AUTHENTICATE);
   1011             if (wwwAuth != null) return wwwAuth.getNonce();
   1012             ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
   1013                     SIPHeaderNames.PROXY_AUTHENTICATE);
   1014             return (proxyAuth == null) ? null : proxyAuth.getNonce();
   1015         }
   1016 
   1017         private String getResponseString(int statusCode) {
   1018             StatusLine statusLine = new StatusLine();
   1019             statusLine.setStatusCode(statusCode);
   1020             statusLine.setReasonPhrase(SIPResponse.getReasonPhrase(statusCode));
   1021             return statusLine.encode();
   1022         }
   1023 
   1024         private boolean readyForCall(EventObject evt) throws SipException {
   1025             // expect MakeCallCommand, RegisterCommand, DEREGISTER
   1026             if (evt instanceof MakeCallCommand) {
   1027                 mState = SipSession.State.OUTGOING_CALL;
   1028                 MakeCallCommand cmd = (MakeCallCommand) evt;
   1029                 mPeerProfile = cmd.getPeerProfile();
   1030                 if (mReferSession != null) {
   1031                     mSipHelper.sendReferNotify(mReferSession.mDialog,
   1032                             getResponseString(Response.TRYING));
   1033                 }
   1034                 mClientTransaction = mSipHelper.sendInvite(
   1035                         mLocalProfile, mPeerProfile, cmd.getSessionDescription(),
   1036                         generateTag(), mReferredBy, mReplaces);
   1037                 mDialog = mClientTransaction.getDialog();
   1038                 addSipSession(this);
   1039                 startSessionTimer(cmd.getTimeout());
   1040                 mProxy.onCalling(this);
   1041                 return true;
   1042             } else if (evt instanceof RegisterCommand) {
   1043                 mState = SipSession.State.REGISTERING;
   1044                 int duration = ((RegisterCommand) evt).getDuration();
   1045                 mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
   1046                         generateTag(), duration);
   1047                 mDialog = mClientTransaction.getDialog();
   1048                 addSipSession(this);
   1049                 mProxy.onRegistering(this);
   1050                 return true;
   1051             } else if (DEREGISTER == evt) {
   1052                 mState = SipSession.State.DEREGISTERING;
   1053                 mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
   1054                         generateTag(), 0);
   1055                 mDialog = mClientTransaction.getDialog();
   1056                 addSipSession(this);
   1057                 mProxy.onRegistering(this);
   1058                 return true;
   1059             }
   1060             return false;
   1061         }
   1062 
   1063         private boolean incomingCall(EventObject evt) throws SipException {
   1064             // expect MakeCallCommand(answering) , END_CALL cmd , Cancel
   1065             if (evt instanceof MakeCallCommand) {
   1066                 // answer call
   1067                 mState = SipSession.State.INCOMING_CALL_ANSWERING;
   1068                 mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived,
   1069                         mLocalProfile,
   1070                         ((MakeCallCommand) evt).getSessionDescription(),
   1071                         mServerTransaction,
   1072                         mExternalIp, mExternalPort);
   1073                 startSessionTimer(((MakeCallCommand) evt).getTimeout());
   1074                 return true;
   1075             } else if (END_CALL == evt) {
   1076                 mSipHelper.sendInviteBusyHere(mInviteReceived,
   1077                         mServerTransaction);
   1078                 endCallNormally();
   1079                 return true;
   1080             } else if (isRequestEvent(Request.CANCEL, evt)) {
   1081                 RequestEvent event = (RequestEvent) evt;
   1082                 mSipHelper.sendResponse(event, Response.OK);
   1083                 mSipHelper.sendInviteRequestTerminated(
   1084                         mInviteReceived.getRequest(), mServerTransaction);
   1085                 endCallNormally();
   1086                 return true;
   1087             }
   1088             return false;
   1089         }
   1090 
   1091         private boolean incomingCallToInCall(EventObject evt)
   1092                 throws SipException {
   1093             // expect ACK, CANCEL request
   1094             if (isRequestEvent(Request.ACK, evt)) {
   1095                 String sdp = extractContent(((RequestEvent) evt).getRequest());
   1096                 if (sdp != null) mPeerSessionDescription = sdp;
   1097                 if (mPeerSessionDescription == null) {
   1098                     onError(SipErrorCode.CLIENT_ERROR, "peer sdp is empty");
   1099                 } else {
   1100                     establishCall(false);
   1101                 }
   1102                 return true;
   1103             } else if (isRequestEvent(Request.CANCEL, evt)) {
   1104                 // http://tools.ietf.org/html/rfc3261#section-9.2
   1105                 // Final response has been sent; do nothing here.
   1106                 return true;
   1107             }
   1108             return false;
   1109         }
   1110 
   1111         private boolean outgoingCall(EventObject evt) throws SipException {
   1112             if (expectResponse(Request.INVITE, evt)) {
   1113                 ResponseEvent event = (ResponseEvent) evt;
   1114                 Response response = event.getResponse();
   1115 
   1116                 int statusCode = response.getStatusCode();
   1117                 switch (statusCode) {
   1118                 case Response.RINGING:
   1119                 case Response.CALL_IS_BEING_FORWARDED:
   1120                 case Response.QUEUED:
   1121                 case Response.SESSION_PROGRESS:
   1122                     // feedback any provisional responses (except TRYING) as
   1123                     // ring back for better UX
   1124                     if (mState == SipSession.State.OUTGOING_CALL) {
   1125                         mState = SipSession.State.OUTGOING_CALL_RING_BACK;
   1126                         cancelSessionTimer();
   1127                         mProxy.onRingingBack(this);
   1128                     }
   1129                     return true;
   1130                 case Response.OK:
   1131                     if (mReferSession != null) {
   1132                         mSipHelper.sendReferNotify(mReferSession.mDialog,
   1133                                 getResponseString(Response.OK));
   1134                         // since we don't need to remember the session anymore.
   1135                         mReferSession = null;
   1136                     }
   1137                     mSipHelper.sendInviteAck(event, mDialog);
   1138                     mPeerSessionDescription = extractContent(response);
   1139                     establishCall(true);
   1140                     return true;
   1141                 case Response.UNAUTHORIZED:
   1142                 case Response.PROXY_AUTHENTICATION_REQUIRED:
   1143                     if (handleAuthentication(event)) {
   1144                         addSipSession(this);
   1145                     }
   1146                     return true;
   1147                 case Response.REQUEST_PENDING:
   1148                     // TODO:
   1149                     // rfc3261#section-14.1; re-schedule invite
   1150                     return true;
   1151                 default:
   1152                     if (mReferSession != null) {
   1153                         mSipHelper.sendReferNotify(mReferSession.mDialog,
   1154                                 getResponseString(Response.SERVICE_UNAVAILABLE));
   1155                     }
   1156                     if (statusCode >= 400) {
   1157                         // error: an ack is sent automatically by the stack
   1158                         onError(response);
   1159                         return true;
   1160                     } else if (statusCode >= 300) {
   1161                         // TODO: handle 3xx (redirect)
   1162                     } else {
   1163                         return true;
   1164                     }
   1165                 }
   1166                 return false;
   1167             } else if (END_CALL == evt) {
   1168                 // RFC says that UA should not send out cancel when no
   1169                 // response comes back yet. We are cheating for not checking
   1170                 // response.
   1171                 mState = SipSession.State.OUTGOING_CALL_CANCELING;
   1172                 mSipHelper.sendCancel(mClientTransaction);
   1173                 startSessionTimer(CANCEL_CALL_TIMER);
   1174                 return true;
   1175             } else if (isRequestEvent(Request.INVITE, evt)) {
   1176                 // Call self? Send BUSY HERE so server may redirect the call to
   1177                 // voice mailbox.
   1178                 RequestEvent event = (RequestEvent) evt;
   1179                 mSipHelper.sendInviteBusyHere(event,
   1180                         event.getServerTransaction());
   1181                 return true;
   1182             }
   1183             return false;
   1184         }
   1185 
   1186         private boolean outgoingCallToReady(EventObject evt)
   1187                 throws SipException {
   1188             if (evt instanceof ResponseEvent) {
   1189                 ResponseEvent event = (ResponseEvent) evt;
   1190                 Response response = event.getResponse();
   1191                 int statusCode = response.getStatusCode();
   1192                 if (expectResponse(Request.CANCEL, evt)) {
   1193                     if (statusCode == Response.OK) {
   1194                         // do nothing; wait for REQUEST_TERMINATED
   1195                         return true;
   1196                     }
   1197                 } else if (expectResponse(Request.INVITE, evt)) {
   1198                     switch (statusCode) {
   1199                         case Response.OK:
   1200                             outgoingCall(evt); // abort Cancel
   1201                             return true;
   1202                         case Response.REQUEST_TERMINATED:
   1203                             endCallNormally();
   1204                             return true;
   1205                     }
   1206                 } else {
   1207                     return false;
   1208                 }
   1209 
   1210                 if (statusCode >= 400) {
   1211                     onError(response);
   1212                     return true;
   1213                 }
   1214             } else if (evt instanceof TransactionTerminatedEvent) {
   1215                 // rfc3261#section-14.1:
   1216                 // if re-invite gets timed out, terminate the dialog; but
   1217                 // re-invite is not reliable, just let it go and pretend
   1218                 // nothing happened.
   1219                 onError(new SipException("timed out"));
   1220             }
   1221             return false;
   1222         }
   1223 
   1224         private boolean processReferRequest(RequestEvent event)
   1225                 throws SipException {
   1226             try {
   1227                 ReferToHeader referto = (ReferToHeader) event.getRequest()
   1228                         .getHeader(ReferTo.NAME);
   1229                 Address address = referto.getAddress();
   1230                 SipURI uri = (SipURI) address.getURI();
   1231                 String replacesHeader = uri.getHeader(ReplacesHeader.NAME);
   1232                 String username = uri.getUser();
   1233                 if (username == null) {
   1234                     mSipHelper.sendResponse(event, Response.BAD_REQUEST);
   1235                     return false;
   1236                 }
   1237                 // send notify accepted
   1238                 mSipHelper.sendResponse(event, Response.ACCEPTED);
   1239                 SipSessionImpl newSession = createNewSession(event,
   1240                         this.mProxy.getListener(),
   1241                         mSipHelper.getServerTransaction(event),
   1242                         SipSession.State.READY_TO_CALL);
   1243                 newSession.mReferSession = this;
   1244                 newSession.mReferredBy = (ReferredByHeader) event.getRequest()
   1245                         .getHeader(ReferredByHeader.NAME);
   1246                 newSession.mReplaces = replacesHeader;
   1247                 newSession.mPeerProfile = createPeerProfile(referto);
   1248                 newSession.mProxy.onCallTransferring(newSession,
   1249                         null);
   1250                 return true;
   1251             } catch (IllegalArgumentException e) {
   1252                 throw new SipException("createPeerProfile()", e);
   1253             }
   1254         }
   1255 
   1256         private boolean inCall(EventObject evt) throws SipException {
   1257             // expect END_CALL cmd, BYE request, hold call (MakeCallCommand)
   1258             // OK retransmission is handled in SipStack
   1259             if (END_CALL == evt) {
   1260                 // rfc3261#section-15.1.1
   1261                 mState = SipSession.State.ENDING_CALL;
   1262                 mSipHelper.sendBye(mDialog);
   1263                 mProxy.onCallEnded(this);
   1264                 startSessionTimer(END_CALL_TIMER);
   1265                 return true;
   1266             } else if (isRequestEvent(Request.INVITE, evt)) {
   1267                 // got Re-INVITE
   1268                 mState = SipSession.State.INCOMING_CALL;
   1269                 RequestEvent event = mInviteReceived = (RequestEvent) evt;
   1270                 mPeerSessionDescription = extractContent(event.getRequest());
   1271                 mServerTransaction = null;
   1272                 mProxy.onRinging(this, mPeerProfile, mPeerSessionDescription);
   1273                 return true;
   1274             } else if (isRequestEvent(Request.BYE, evt)) {
   1275                 mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
   1276                 endCallNormally();
   1277                 return true;
   1278             } else if (isRequestEvent(Request.REFER, evt)) {
   1279                 return processReferRequest((RequestEvent) evt);
   1280             } else if (evt instanceof MakeCallCommand) {
   1281                 // to change call
   1282                 mState = SipSession.State.OUTGOING_CALL;
   1283                 mClientTransaction = mSipHelper.sendReinvite(mDialog,
   1284                         ((MakeCallCommand) evt).getSessionDescription());
   1285                 startSessionTimer(((MakeCallCommand) evt).getTimeout());
   1286                 return true;
   1287             } else if (evt instanceof ResponseEvent) {
   1288                 if (expectResponse(Request.NOTIFY, evt)) return true;
   1289             }
   1290             return false;
   1291         }
   1292 
   1293         private boolean endingCall(EventObject evt) throws SipException {
   1294             if (expectResponse(Request.BYE, evt)) {
   1295                 ResponseEvent event = (ResponseEvent) evt;
   1296                 Response response = event.getResponse();
   1297 
   1298                 int statusCode = response.getStatusCode();
   1299                 switch (statusCode) {
   1300                     case Response.UNAUTHORIZED:
   1301                     case Response.PROXY_AUTHENTICATION_REQUIRED:
   1302                         if (handleAuthentication(event)) {
   1303                             return true;
   1304                         } else {
   1305                             // can't authenticate; pass through to end session
   1306                         }
   1307                 }
   1308                 cancelSessionTimer();
   1309                 reset();
   1310                 return true;
   1311             }
   1312             return false;
   1313         }
   1314 
   1315         // timeout in seconds
   1316         private void startSessionTimer(int timeout) {
   1317             if (timeout > 0) {
   1318                 mSessionTimer = new SessionTimer();
   1319                 mSessionTimer.start(timeout);
   1320             }
   1321         }
   1322 
   1323         private void cancelSessionTimer() {
   1324             if (mSessionTimer != null) {
   1325                 mSessionTimer.cancel();
   1326                 mSessionTimer = null;
   1327             }
   1328         }
   1329 
   1330         private String createErrorMessage(Response response) {
   1331             return String.format("%s (%d)", response.getReasonPhrase(),
   1332                     response.getStatusCode());
   1333         }
   1334 
   1335         private void enableKeepAlive() {
   1336             if (mKeepAliveSession != null) {
   1337                 mKeepAliveSession.stopKeepAliveProcess();
   1338             } else {
   1339                 mKeepAliveSession = duplicate();
   1340             }
   1341             try {
   1342                 mKeepAliveSession.startKeepAliveProcess(
   1343                         INCALL_KEEPALIVE_INTERVAL, mPeerProfile, null);
   1344             } catch (SipException e) {
   1345                 Log.w(TAG, "keepalive cannot be enabled; ignored", e);
   1346                 mKeepAliveSession.stopKeepAliveProcess();
   1347             }
   1348         }
   1349 
   1350         private void establishCall(boolean enableKeepAlive) {
   1351             mState = SipSession.State.IN_CALL;
   1352             cancelSessionTimer();
   1353             if (!mInCall && enableKeepAlive) enableKeepAlive();
   1354             mInCall = true;
   1355             mProxy.onCallEstablished(this, mPeerSessionDescription);
   1356         }
   1357 
   1358         private void endCallNormally() {
   1359             reset();
   1360             mProxy.onCallEnded(this);
   1361         }
   1362 
   1363         private void endCallOnError(int errorCode, String message) {
   1364             reset();
   1365             mProxy.onError(this, errorCode, message);
   1366         }
   1367 
   1368         private void endCallOnBusy() {
   1369             reset();
   1370             mProxy.onCallBusy(this);
   1371         }
   1372 
   1373         private void onError(int errorCode, String message) {
   1374             cancelSessionTimer();
   1375             switch (mState) {
   1376                 case SipSession.State.REGISTERING:
   1377                 case SipSession.State.DEREGISTERING:
   1378                     onRegistrationFailed(errorCode, message);
   1379                     break;
   1380                 default:
   1381                     endCallOnError(errorCode, message);
   1382             }
   1383         }
   1384 
   1385 
   1386         private void onError(Throwable exception) {
   1387             exception = getRootCause(exception);
   1388             onError(getErrorCode(exception), exception.toString());
   1389         }
   1390 
   1391         private void onError(Response response) {
   1392             int statusCode = response.getStatusCode();
   1393             if (!mInCall && (statusCode == Response.BUSY_HERE)) {
   1394                 endCallOnBusy();
   1395             } else {
   1396                 onError(getErrorCode(statusCode), createErrorMessage(response));
   1397             }
   1398         }
   1399 
   1400         private int getErrorCode(int responseStatusCode) {
   1401             switch (responseStatusCode) {
   1402                 case Response.TEMPORARILY_UNAVAILABLE:
   1403                 case Response.FORBIDDEN:
   1404                 case Response.GONE:
   1405                 case Response.NOT_FOUND:
   1406                 case Response.NOT_ACCEPTABLE:
   1407                 case Response.NOT_ACCEPTABLE_HERE:
   1408                     return SipErrorCode.PEER_NOT_REACHABLE;
   1409 
   1410                 case Response.REQUEST_URI_TOO_LONG:
   1411                 case Response.ADDRESS_INCOMPLETE:
   1412                 case Response.AMBIGUOUS:
   1413                     return SipErrorCode.INVALID_REMOTE_URI;
   1414 
   1415                 case Response.REQUEST_TIMEOUT:
   1416                     return SipErrorCode.TIME_OUT;
   1417 
   1418                 default:
   1419                     if (responseStatusCode < 500) {
   1420                         return SipErrorCode.CLIENT_ERROR;
   1421                     } else {
   1422                         return SipErrorCode.SERVER_ERROR;
   1423                     }
   1424             }
   1425         }
   1426 
   1427         private int getErrorCode(Throwable exception) {
   1428             String message = exception.getMessage();
   1429             if (exception instanceof UnknownHostException) {
   1430                 return SipErrorCode.SERVER_UNREACHABLE;
   1431             } else if (exception instanceof IOException) {
   1432                 return SipErrorCode.SOCKET_ERROR;
   1433             } else {
   1434                 return SipErrorCode.CLIENT_ERROR;
   1435             }
   1436         }
   1437 
   1438         private void onRegistrationDone(int duration) {
   1439             reset();
   1440             mProxy.onRegistrationDone(this, duration);
   1441         }
   1442 
   1443         private void onRegistrationFailed(int errorCode, String message) {
   1444             reset();
   1445             mProxy.onRegistrationFailed(this, errorCode, message);
   1446         }
   1447 
   1448         private void onRegistrationFailed(Throwable exception) {
   1449             exception = getRootCause(exception);
   1450             onRegistrationFailed(getErrorCode(exception),
   1451                     exception.toString());
   1452         }
   1453 
   1454         private void onRegistrationFailed(Response response) {
   1455             int statusCode = response.getStatusCode();
   1456             onRegistrationFailed(getErrorCode(statusCode),
   1457                     createErrorMessage(response));
   1458         }
   1459 
   1460         // Notes: SipSessionListener will be replaced by the keepalive process
   1461         // @param interval in seconds
   1462         public void startKeepAliveProcess(int interval,
   1463                 KeepAliveProcessCallback callback) throws SipException {
   1464             synchronized (SipSessionGroup.this) {
   1465                 startKeepAliveProcess(interval, mLocalProfile, callback);
   1466             }
   1467         }
   1468 
   1469         // Notes: SipSessionListener will be replaced by the keepalive process
   1470         // @param interval in seconds
   1471         public void startKeepAliveProcess(int interval, SipProfile peerProfile,
   1472                 KeepAliveProcessCallback callback) throws SipException {
   1473             synchronized (SipSessionGroup.this) {
   1474                 if (mKeepAliveProcess != null) {
   1475                     throw new SipException("Cannot create more than one "
   1476                             + "keepalive process in a SipSession");
   1477                 }
   1478                 mPeerProfile = peerProfile;
   1479                 mKeepAliveProcess = new KeepAliveProcess();
   1480                 mProxy.setListener(mKeepAliveProcess);
   1481                 mKeepAliveProcess.start(interval, callback);
   1482             }
   1483         }
   1484 
   1485         public void stopKeepAliveProcess() {
   1486             synchronized (SipSessionGroup.this) {
   1487                 if (mKeepAliveProcess != null) {
   1488                     mKeepAliveProcess.stop();
   1489                     mKeepAliveProcess = null;
   1490                 }
   1491             }
   1492         }
   1493 
   1494         class KeepAliveProcess extends SipSessionAdapter implements Runnable {
   1495             private static final String TAG = "SipKeepAlive";
   1496             private boolean mRunning = false;
   1497             private KeepAliveProcessCallback mCallback;
   1498 
   1499             private boolean mPortChanged = false;
   1500             private int mRPort = 0;
   1501             private int mInterval; // just for debugging
   1502 
   1503             // @param interval in seconds
   1504             void start(int interval, KeepAliveProcessCallback callback) {
   1505                 if (mRunning) return;
   1506                 mRunning = true;
   1507                 mInterval = interval;
   1508                 mCallback = new KeepAliveProcessCallbackProxy(callback);
   1509                 mWakeupTimer.set(interval * 1000, this);
   1510                 if (DEBUG) {
   1511                     Log.d(TAG, "start keepalive:"
   1512                             + mLocalProfile.getUriString());
   1513                 }
   1514 
   1515                 // No need to run the first time in a separate thread for now
   1516                 run();
   1517             }
   1518 
   1519             // return true if the event is consumed
   1520             boolean process(EventObject evt) throws SipException {
   1521                 if (mRunning && (mState == SipSession.State.PINGING)) {
   1522                     if (evt instanceof ResponseEvent) {
   1523                         if (parseOptionsResult(evt)) {
   1524                             if (mPortChanged) {
   1525                                 resetExternalAddress();
   1526                                 stop();
   1527                             } else {
   1528                                 cancelSessionTimer();
   1529                                 removeSipSession(SipSessionImpl.this);
   1530                             }
   1531                             mCallback.onResponse(mPortChanged);
   1532                             return true;
   1533                         }
   1534                     }
   1535                 }
   1536                 return false;
   1537             }
   1538 
   1539             // SipSessionAdapter
   1540             // To react to the session timeout event and network error.
   1541             @Override
   1542             public void onError(ISipSession session, int errorCode, String message) {
   1543                 stop();
   1544                 mCallback.onError(errorCode, message);
   1545             }
   1546 
   1547             // SipWakeupTimer timeout handler
   1548             // To send out keepalive message.
   1549             @Override
   1550             public void run() {
   1551                 synchronized (SipSessionGroup.this) {
   1552                     if (!mRunning) return;
   1553 
   1554                     if (DEBUG_PING) {
   1555                         String peerUri = (mPeerProfile == null)
   1556                                 ? "null"
   1557                                 : mPeerProfile.getUriString();
   1558                         Log.d(TAG, "keepalive: " + mLocalProfile.getUriString()
   1559                                 + " --> " + peerUri + ", interval=" + mInterval);
   1560                     }
   1561                     try {
   1562                         sendKeepAlive();
   1563                     } catch (Throwable t) {
   1564                         if (DEBUG) {
   1565                             Log.w(TAG, "keepalive error: "
   1566                                     + mLocalProfile.getUriString(), getRootCause(t));
   1567                         }
   1568                         // It's possible that the keepalive process is being stopped
   1569                         // during session.sendKeepAlive() so need to check mRunning
   1570                         // again here.
   1571                         if (mRunning) SipSessionImpl.this.onError(t);
   1572                     }
   1573                 }
   1574             }
   1575 
   1576             void stop() {
   1577                 synchronized (SipSessionGroup.this) {
   1578                     if (DEBUG) {
   1579                         Log.d(TAG, "stop keepalive:" + mLocalProfile.getUriString()
   1580                                 + ",RPort=" + mRPort);
   1581                     }
   1582                     mRunning = false;
   1583                     mWakeupTimer.cancel(this);
   1584                     reset();
   1585                 }
   1586             }
   1587 
   1588             private void sendKeepAlive() throws SipException, InterruptedException {
   1589                 synchronized (SipSessionGroup.this) {
   1590                     mState = SipSession.State.PINGING;
   1591                     mClientTransaction = mSipHelper.sendOptions(
   1592                             mLocalProfile, mPeerProfile, generateTag());
   1593                     mDialog = mClientTransaction.getDialog();
   1594                     addSipSession(SipSessionImpl.this);
   1595 
   1596                     startSessionTimer(KEEPALIVE_TIMEOUT);
   1597                     // when timed out, onError() will be called with SipErrorCode.TIME_OUT
   1598                 }
   1599             }
   1600 
   1601             private boolean parseOptionsResult(EventObject evt) {
   1602                 if (expectResponse(Request.OPTIONS, evt)) {
   1603                     ResponseEvent event = (ResponseEvent) evt;
   1604                     int rPort = getRPortFromResponse(event.getResponse());
   1605                     if (rPort != -1) {
   1606                         if (mRPort == 0) mRPort = rPort;
   1607                         if (mRPort != rPort) {
   1608                             mPortChanged = true;
   1609                             if (DEBUG) Log.d(TAG, String.format(
   1610                                     "rport is changed: %d <> %d", mRPort, rPort));
   1611                             mRPort = rPort;
   1612                         } else {
   1613                             if (DEBUG) Log.d(TAG, "rport is the same: " + rPort);
   1614                         }
   1615                     } else {
   1616                         if (DEBUG) Log.w(TAG, "peer did not respond rport");
   1617                     }
   1618                     return true;
   1619                 }
   1620                 return false;
   1621             }
   1622 
   1623             private int getRPortFromResponse(Response response) {
   1624                 ViaHeader viaHeader = (ViaHeader)(response.getHeader(
   1625                         SIPHeaderNames.VIA));
   1626                 return (viaHeader == null) ? -1 : viaHeader.getRPort();
   1627             }
   1628         }
   1629     }
   1630 
   1631     /**
   1632      * @return true if the event is a request event matching the specified
   1633      *      method; false otherwise
   1634      */
   1635     private static boolean isRequestEvent(String method, EventObject event) {
   1636         try {
   1637             if (event instanceof RequestEvent) {
   1638                 RequestEvent requestEvent = (RequestEvent) event;
   1639                 return method.equals(requestEvent.getRequest().getMethod());
   1640             }
   1641         } catch (Throwable e) {
   1642         }
   1643         return false;
   1644     }
   1645 
   1646     private static String getCseqMethod(Message message) {
   1647         return ((CSeqHeader) message.getHeader(CSeqHeader.NAME)).getMethod();
   1648     }
   1649 
   1650     /**
   1651      * @return true if the event is a response event and the CSeqHeader method
   1652      * match the given arguments; false otherwise
   1653      */
   1654     private static boolean expectResponse(
   1655             String expectedMethod, EventObject evt) {
   1656         if (evt instanceof ResponseEvent) {
   1657             ResponseEvent event = (ResponseEvent) evt;
   1658             Response response = event.getResponse();
   1659             return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
   1660         }
   1661         return false;
   1662     }
   1663 
   1664     /**
   1665      * @return true if the event is a response event and the response code and
   1666      *      CSeqHeader method match the given arguments; false otherwise
   1667      */
   1668     private static boolean expectResponse(
   1669             int responseCode, String expectedMethod, EventObject evt) {
   1670         if (evt instanceof ResponseEvent) {
   1671             ResponseEvent event = (ResponseEvent) evt;
   1672             Response response = event.getResponse();
   1673             if (response.getStatusCode() == responseCode) {
   1674                 return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
   1675             }
   1676         }
   1677         return false;
   1678     }
   1679 
   1680     private static SipProfile createPeerProfile(HeaderAddress header)
   1681             throws SipException {
   1682         try {
   1683             Address address = header.getAddress();
   1684             SipURI uri = (SipURI) address.getURI();
   1685             String username = uri.getUser();
   1686             if (username == null) username = ANONYMOUS;
   1687             int port = uri.getPort();
   1688             SipProfile.Builder builder =
   1689                     new SipProfile.Builder(username, uri.getHost())
   1690                     .setDisplayName(address.getDisplayName());
   1691             if (port > 0) builder.setPort(port);
   1692             return builder.build();
   1693         } catch (IllegalArgumentException e) {
   1694             throw new SipException("createPeerProfile()", e);
   1695         } catch (ParseException e) {
   1696             throw new SipException("createPeerProfile()", e);
   1697         }
   1698     }
   1699 
   1700     private static boolean isLoggable(SipSessionImpl s) {
   1701         if (s != null) {
   1702             switch (s.mState) {
   1703                 case SipSession.State.PINGING:
   1704                     return DEBUG_PING;
   1705             }
   1706         }
   1707         return DEBUG;
   1708     }
   1709 
   1710     private static boolean isLoggable(EventObject evt) {
   1711         return isLoggable(null, evt);
   1712     }
   1713 
   1714     private static boolean isLoggable(SipSessionImpl s, EventObject evt) {
   1715         if (!isLoggable(s)) return false;
   1716         if (evt == null) return false;
   1717 
   1718         if (evt instanceof ResponseEvent) {
   1719             Response response = ((ResponseEvent) evt).getResponse();
   1720             if (Request.OPTIONS.equals(response.getHeader(CSeqHeader.NAME))) {
   1721                 return DEBUG_PING;
   1722             }
   1723             return DEBUG;
   1724         } else if (evt instanceof RequestEvent) {
   1725             if (isRequestEvent(Request.OPTIONS, evt)) {
   1726                 return DEBUG_PING;
   1727             }
   1728             return DEBUG;
   1729         }
   1730         return false;
   1731     }
   1732 
   1733     private static String log(EventObject evt) {
   1734         if (evt instanceof RequestEvent) {
   1735             return ((RequestEvent) evt).getRequest().toString();
   1736         } else if (evt instanceof ResponseEvent) {
   1737             return ((ResponseEvent) evt).getResponse().toString();
   1738         } else {
   1739             return evt.toString();
   1740         }
   1741     }
   1742 
   1743     private class RegisterCommand extends EventObject {
   1744         private int mDuration;
   1745 
   1746         public RegisterCommand(int duration) {
   1747             super(SipSessionGroup.this);
   1748             mDuration = duration;
   1749         }
   1750 
   1751         public int getDuration() {
   1752             return mDuration;
   1753         }
   1754     }
   1755 
   1756     private class MakeCallCommand extends EventObject {
   1757         private String mSessionDescription;
   1758         private int mTimeout; // in seconds
   1759 
   1760         public MakeCallCommand(SipProfile peerProfile,
   1761                 String sessionDescription) {
   1762             this(peerProfile, sessionDescription, -1);
   1763         }
   1764 
   1765         public MakeCallCommand(SipProfile peerProfile,
   1766                 String sessionDescription, int timeout) {
   1767             super(peerProfile);
   1768             mSessionDescription = sessionDescription;
   1769             mTimeout = timeout;
   1770         }
   1771 
   1772         public SipProfile getPeerProfile() {
   1773             return (SipProfile) getSource();
   1774         }
   1775 
   1776         public String getSessionDescription() {
   1777             return mSessionDescription;
   1778         }
   1779 
   1780         public int getTimeout() {
   1781             return mTimeout;
   1782         }
   1783     }
   1784 
   1785     /** Class to help safely run KeepAliveProcessCallback in a different thread. */
   1786     static class KeepAliveProcessCallbackProxy implements KeepAliveProcessCallback {
   1787         private KeepAliveProcessCallback mCallback;
   1788 
   1789         KeepAliveProcessCallbackProxy(KeepAliveProcessCallback callback) {
   1790             mCallback = callback;
   1791         }
   1792 
   1793         private void proxy(Runnable runnable) {
   1794             // One thread for each calling back.
   1795             // Note: Guarantee ordering if the issue becomes important. Currently,
   1796             // the chance of handling two callback events at a time is none.
   1797             new Thread(runnable, "SIP-KeepAliveProcessCallbackThread").start();
   1798         }
   1799 
   1800         public void onResponse(final boolean portChanged) {
   1801             if (mCallback == null) return;
   1802             proxy(new Runnable() {
   1803                 public void run() {
   1804                     try {
   1805                         mCallback.onResponse(portChanged);
   1806                     } catch (Throwable t) {
   1807                         Log.w(TAG, "onResponse", t);
   1808                     }
   1809                 }
   1810             });
   1811         }
   1812 
   1813         public void onError(final int errorCode, final String description) {
   1814             if (mCallback == null) return;
   1815             proxy(new Runnable() {
   1816                 public void run() {
   1817                     try {
   1818                         mCallback.onError(errorCode, description);
   1819                     } catch (Throwable t) {
   1820                         Log.w(TAG, "onError", t);
   1821                     }
   1822                 }
   1823             });
   1824         }
   1825     }
   1826 }
   1827