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      */
SipSessionGroup(SipProfile profile, String password, SipWakeupTimer timer, SipWakeLock wakeLock)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
setWakeupTimer(SipWakeupTimer timer)142     void setWakeupTimer(SipWakeupTimer timer) {
143         mWakeupTimer = timer;
144     }
145 
reset()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 
onConnectivityChanged()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 
resetExternalAddress()219     synchronized void resetExternalAddress() {
220         if (DBG) {
221             log("resetExternalAddress: " + mSipStack);
222         }
223         mExternalIp = null;
224         mExternalPort = 0;
225     }
226 
getLocalProfile()227     public SipProfile getLocalProfile() {
228         return mLocalProfile;
229     }
230 
getLocalProfileUri()231     public String getLocalProfileUri() {
232         return mLocalProfile.getUriString();
233     }
234 
getStackName()235     private String getStackName() {
236         return "stack" + System.currentTimeMillis();
237     }
238 
close()239     public synchronized void close() {
240         if (DBG) log("close: " + SipService.obfuscateSipUri(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 
isClosed()252     public synchronized boolean isClosed() {
253         return (mSipStack == null);
254     }
255 
256     // For internal use, require listener not to block in callbacks.
openToReceiveCalls(ISipSessionListener listener)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 
closeToNotReceiveCalls()265     public synchronized void closeToNotReceiveCalls() {
266         mCallReceiverSession = null;
267     }
268 
createSession(ISipSessionListener listener)269     public ISipSession createSession(ISipSessionListener listener) {
270         return (isClosed() ? null : new SipSessionImpl(listener));
271     }
272 
containsSession(String callId)273     synchronized boolean containsSession(String callId) {
274         return mSessionMap.containsKey(callId);
275     }
276 
getSipSession(EventObject event)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 
addSipSession(SipSessionImpl newSession)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 
removeSipSession(SipSessionImpl session)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
processRequest(final RequestEvent event)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
processResponse(ResponseEvent event)341     public void processResponse(ResponseEvent event) {
342         process(event);
343     }
344 
345     @Override
processIOException(IOExceptionEvent event)346     public void processIOException(IOExceptionEvent event) {
347         process(event);
348     }
349 
350     @Override
processTimeout(TimeoutEvent event)351     public void processTimeout(TimeoutEvent event) {
352         process(event);
353     }
354 
355     @Override
processTransactionTerminated(TransactionTerminatedEvent event)356     public void processTransactionTerminated(TransactionTerminatedEvent event) {
357         process(event);
358     }
359 
360     @Override
processDialogTerminated(DialogTerminatedEvent event)361     public void processDialogTerminated(DialogTerminatedEvent event) {
362         process(event);
363     }
364 
process(EventObject event)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 
extractContent(Message message)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 
extractExternalAddress(ResponseEvent evt)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 
getRootCause(Throwable exception)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 
createNewSession(RequestEvent event, ISipSessionListener listener, ServerTransaction transaction, int newState)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 
SipSessionCallReceiverImpl(ISipSessionListener listener)441         public SipSessionCallReceiverImpl(ISipSessionListener listener) {
442             super(listener);
443         }
444 
processInviteWithReplaces(RequestEvent event, ReplacesHeader replaces)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 
processNewInviteRequest(RequestEvent event)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
process(EventObject evt)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 
log(String s)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. */
onResponse(boolean portChanged)529         void onResponse(boolean portChanged);
onError(int errorCode, String description)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 
start(final int timeout)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 
cancel()572             synchronized void cancel() {
573                 mRunning = false;
574                 this.notify();
575             }
576 
timeout()577             private void timeout() {
578                 synchronized (SipSessionGroup.this) {
579                     onError(SipErrorCode.TIME_OUT, "Session timed out!");
580                 }
581             }
582 
sleep(int timeout)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 
SipSessionImpl(ISipSessionListener listener)592         public SipSessionImpl(ISipSessionListener listener) {
593             setListener(listener);
594         }
595 
duplicate()596         SipSessionImpl duplicate() {
597             return new SipSessionImpl(mProxy.getListener());
598         }
599 
reset()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
isInCall()638         public boolean isInCall() {
639             return mInCall;
640         }
641 
642         @Override
getLocalIp()643         public String getLocalIp() {
644             return mLocalIp;
645         }
646 
647         @Override
getLocalProfile()648         public SipProfile getLocalProfile() {
649             return mLocalProfile;
650         }
651 
652         @Override
getPeerProfile()653         public SipProfile getPeerProfile() {
654             return mPeerProfile;
655         }
656 
657         @Override
getCallId()658         public String getCallId() {
659             return SipHelper.getCallId(getTransaction());
660         }
661 
getTransaction()662         private Transaction getTransaction() {
663             if (mClientTransaction != null) return mClientTransaction;
664             if (mServerTransaction != null) return mServerTransaction;
665             return null;
666         }
667 
668         @Override
getState()669         public int getState() {
670             return mState;
671         }
672 
673         @Override
setListener(ISipSessionListener listener)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
doCommandAsync(final EventObject command)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
makeCall(SipProfile peerProfile, String sessionDescription, int timeout)698         public void makeCall(SipProfile peerProfile, String sessionDescription,
699                 int timeout) {
700             doCommandAsync(new MakeCallCommand(peerProfile, sessionDescription,
701                     timeout));
702         }
703 
704         @Override
answerCall(String sessionDescription, int timeout)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
endCall()714         public void endCall() {
715             doCommandAsync(END_CALL);
716         }
717 
718         @Override
changeCall(String sessionDescription, int timeout)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
register(int duration)728         public void register(int duration) {
729             doCommandAsync(new RegisterCommand(duration));
730         }
731 
732         @Override
unregister()733         public void unregister() {
734             doCommandAsync(DEREGISTER);
735         }
736 
processCommand(EventObject command)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 
generateTag()746         protected String generateTag() {
747             // 32-bit randomness
748             return String.valueOf((long) (Math.random() * 0x100000000L));
749         }
750 
751         @Override
toString()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 
process(EventObject evt)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 
processExceptions(EventObject evt)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 
processDialogTerminated(DialogTerminatedEvent event)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 
isCurrentTransaction(TransactionTerminatedEvent event)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 
toString(Transaction transaction)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 
processTransactionTerminated( TransactionTerminatedEvent event)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 
processTimeout(TimeoutEvent event)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 
getExpiryTime(Response response)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 
registeringToReady(EventObject evt)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 
handleAuthentication(ResponseEvent event)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 
crossDomainAuthenticationRequired(Response response)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 
getAccountManager()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