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