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