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