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