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