SipSessionGroup.java revision 4189d99b6e4877352049b7447b7f0734ef99b9e8
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.SIPHeaderNames;
22import gov.nist.javax.sip.header.ProxyAuthenticate;
23import gov.nist.javax.sip.header.WWWAuthenticate;
24import gov.nist.javax.sip.message.SIPMessage;
25
26import android.net.sip.ISipSession;
27import android.net.sip.ISipSessionListener;
28import android.net.sip.SipErrorCode;
29import android.net.sip.SipProfile;
30import android.net.sip.SipSession;
31import android.text.TextUtils;
32import android.util.Log;
33
34import java.io.IOException;
35import java.io.UnsupportedEncodingException;
36import java.net.DatagramSocket;
37import java.net.UnknownHostException;
38import java.text.ParseException;
39import java.util.Collection;
40import java.util.EventObject;
41import java.util.HashMap;
42import java.util.Map;
43import java.util.Properties;
44import java.util.TooManyListenersException;
45
46import javax.sip.ClientTransaction;
47import javax.sip.Dialog;
48import javax.sip.DialogTerminatedEvent;
49import javax.sip.IOExceptionEvent;
50import javax.sip.InvalidArgumentException;
51import javax.sip.ListeningPoint;
52import javax.sip.ObjectInUseException;
53import javax.sip.RequestEvent;
54import javax.sip.ResponseEvent;
55import javax.sip.ServerTransaction;
56import javax.sip.SipException;
57import javax.sip.SipFactory;
58import javax.sip.SipListener;
59import javax.sip.SipProvider;
60import javax.sip.SipStack;
61import javax.sip.TimeoutEvent;
62import javax.sip.Transaction;
63import javax.sip.TransactionState;
64import javax.sip.TransactionTerminatedEvent;
65import javax.sip.TransactionUnavailableException;
66import javax.sip.address.Address;
67import javax.sip.address.SipURI;
68import javax.sip.header.CSeqHeader;
69import javax.sip.header.ExpiresHeader;
70import javax.sip.header.FromHeader;
71import javax.sip.header.MinExpiresHeader;
72import javax.sip.header.ViaHeader;
73import javax.sip.message.Message;
74import javax.sip.message.Request;
75import javax.sip.message.Response;
76
77/**
78 * Manages {@link ISipSession}'s for a SIP account.
79 */
80class SipSessionGroup implements SipListener {
81    private static final String TAG = "SipSession";
82    private static final boolean DEBUG = true;
83    private static final boolean DEBUG_PING = DEBUG && false;
84    private static final String ANONYMOUS = "anonymous";
85    // Limit the size of thread pool to 1 for the order issue when the phone is
86    // waken up from sleep and there are many packets to be processed in the SIP
87    // stack. Note: The default thread pool size in NIST SIP stack is -1 which is
88    // unlimited.
89    private static final String THREAD_POOL_SIZE = "1";
90    private static final int EXPIRY_TIME = 3600; // in seconds
91    private static final int CANCEL_CALL_TIMER = 3; // in seconds
92    private static final long WAKE_LOCK_HOLDING_TIME = 500; // in milliseconds
93
94    private static final EventObject DEREGISTER = new EventObject("Deregister");
95    private static final EventObject END_CALL = new EventObject("End call");
96    private static final EventObject HOLD_CALL = new EventObject("Hold call");
97    private static final EventObject CONTINUE_CALL
98            = new EventObject("Continue call");
99
100    private final SipProfile mLocalProfile;
101    private final String mPassword;
102
103    private SipStack mSipStack;
104    private SipHelper mSipHelper;
105
106    // session that processes INVITE requests
107    private SipSessionImpl mCallReceiverSession;
108    private String mLocalIp;
109
110    private SipWakeLock mWakeLock;
111
112    // call-id-to-SipSession map
113    private Map<String, SipSessionImpl> mSessionMap =
114            new HashMap<String, SipSessionImpl>();
115
116    /**
117     * @param myself the local profile with password crossed out
118     * @param password the password of the profile
119     * @throws IOException if cannot assign requested address
120     */
121    public SipSessionGroup(String localIp, SipProfile myself, String password,
122            SipWakeLock wakeLock) throws SipException, IOException {
123        mLocalProfile = myself;
124        mPassword = password;
125        mWakeLock = wakeLock;
126        reset(localIp);
127    }
128
129    synchronized void reset(String localIp) throws SipException, IOException {
130        mLocalIp = localIp;
131        if (localIp == null) return;
132
133        SipProfile myself = mLocalProfile;
134        SipFactory sipFactory = SipFactory.getInstance();
135        Properties properties = new Properties();
136        properties.setProperty("javax.sip.STACK_NAME", getStackName());
137        properties.setProperty(
138                "gov.nist.javax.sip.THREAD_POOL_SIZE", THREAD_POOL_SIZE);
139        String outboundProxy = myself.getProxyAddress();
140        if (!TextUtils.isEmpty(outboundProxy)) {
141            Log.v(TAG, "outboundProxy is " + outboundProxy);
142            properties.setProperty("javax.sip.OUTBOUND_PROXY", outboundProxy
143                    + ":" + myself.getPort() + "/" + myself.getProtocol());
144        }
145        SipStack stack = mSipStack = sipFactory.createSipStack(properties);
146
147        try {
148            SipProvider provider = stack.createSipProvider(
149                    stack.createListeningPoint(localIp, allocateLocalPort(),
150                            myself.getProtocol()));
151            provider.addSipListener(this);
152            mSipHelper = new SipHelper(stack, provider);
153        } catch (InvalidArgumentException e) {
154            throw new IOException(e.getMessage());
155        } catch (TooManyListenersException e) {
156            // must never happen
157            throw new SipException("SipSessionGroup constructor", e);
158        }
159        Log.d(TAG, " start stack for " + myself.getUriString());
160        stack.start();
161
162        mCallReceiverSession = null;
163        mSessionMap.clear();
164    }
165
166    synchronized void onConnectivityChanged() {
167        SipSessionImpl[] ss = mSessionMap.values().toArray(
168                    new SipSessionImpl[mSessionMap.size()]);
169        // Iterate on the copied array instead of directly on mSessionMap to
170        // avoid ConcurrentModificationException being thrown when
171        // SipSessionImpl removes itself from mSessionMap in onError() in the
172        // following loop.
173        for (SipSessionImpl s : ss) {
174            s.onError(SipErrorCode.DATA_CONNECTION_LOST,
175                    "data connection lost");
176        }
177    }
178
179    public SipProfile getLocalProfile() {
180        return mLocalProfile;
181    }
182
183    public String getLocalProfileUri() {
184        return mLocalProfile.getUriString();
185    }
186
187    private String getStackName() {
188        return "stack" + System.currentTimeMillis();
189    }
190
191    public synchronized void close() {
192        Log.d(TAG, " close stack for " + mLocalProfile.getUriString());
193        onConnectivityChanged();
194        mSessionMap.clear();
195        closeToNotReceiveCalls();
196        if (mSipStack != null) {
197            mSipStack.stop();
198            mSipStack = null;
199            mSipHelper = null;
200        }
201    }
202
203    public synchronized boolean isClosed() {
204        return (mSipStack == null);
205    }
206
207    // For internal use, require listener not to block in callbacks.
208    public synchronized void openToReceiveCalls(ISipSessionListener listener) {
209        if (mCallReceiverSession == null) {
210            mCallReceiverSession = new SipSessionCallReceiverImpl(listener);
211        } else {
212            mCallReceiverSession.setListener(listener);
213        }
214    }
215
216    public synchronized void closeToNotReceiveCalls() {
217        mCallReceiverSession = null;
218    }
219
220    public ISipSession createSession(ISipSessionListener listener) {
221        return (isClosed() ? null : new SipSessionImpl(listener));
222    }
223
224    private static int allocateLocalPort() throws SipException {
225        try {
226            DatagramSocket s = new DatagramSocket();
227            int localPort = s.getLocalPort();
228            s.close();
229            return localPort;
230        } catch (IOException e) {
231            throw new SipException("allocateLocalPort()", e);
232        }
233    }
234
235    synchronized boolean containsSession(String callId) {
236        return mSessionMap.containsKey(callId);
237    }
238
239    private synchronized SipSessionImpl getSipSession(EventObject event) {
240        String key = SipHelper.getCallId(event);
241        SipSessionImpl session = mSessionMap.get(key);
242        if ((session != null) && isLoggable(session)) {
243            Log.d(TAG, "session key from event: " + key);
244            Log.d(TAG, "active sessions:");
245            for (String k : mSessionMap.keySet()) {
246                Log.d(TAG, " ..." + k + ": " + mSessionMap.get(k));
247            }
248        }
249        return ((session != null) ? session : mCallReceiverSession);
250    }
251
252    private synchronized void addSipSession(SipSessionImpl newSession) {
253        removeSipSession(newSession);
254        String key = newSession.getCallId();
255        mSessionMap.put(key, newSession);
256        if (isLoggable(newSession)) {
257            Log.d(TAG, "+++  add a session with key:  '" + key + "'");
258            for (String k : mSessionMap.keySet()) {
259                Log.d(TAG, "  " + k + ": " + mSessionMap.get(k));
260            }
261        }
262    }
263
264    private synchronized void removeSipSession(SipSessionImpl session) {
265        if (session == mCallReceiverSession) return;
266        String key = session.getCallId();
267        SipSessionImpl s = mSessionMap.remove(key);
268        // sanity check
269        if ((s != null) && (s != session)) {
270            Log.w(TAG, "session " + session + " is not associated with key '"
271                    + key + "'");
272            mSessionMap.put(key, s);
273            for (Map.Entry<String, SipSessionImpl> entry
274                    : mSessionMap.entrySet()) {
275                if (entry.getValue() == s) {
276                    key = entry.getKey();
277                    mSessionMap.remove(key);
278                }
279            }
280        }
281
282        if ((s != null) && isLoggable(s)) {
283            Log.d(TAG, "remove session " + session + " @key '" + key + "'");
284            for (String k : mSessionMap.keySet()) {
285                Log.d(TAG, "  " + k + ": " + mSessionMap.get(k));
286            }
287        }
288    }
289
290    public void processRequest(final RequestEvent event) {
291        if (isRequestEvent(Request.INVITE, event)) {
292            if (DEBUG) Log.d(TAG, "<<<<< got INVITE, thread:"
293                    + Thread.currentThread());
294            // Acquire a wake lock and keep it for WAKE_LOCK_HOLDING_TIME;
295            // should be large enough to bring up the app.
296            mWakeLock.acquire(WAKE_LOCK_HOLDING_TIME);
297        }
298        process(event);
299    }
300
301    public void processResponse(ResponseEvent event) {
302        process(event);
303    }
304
305    public void processIOException(IOExceptionEvent event) {
306        process(event);
307    }
308
309    public void processTimeout(TimeoutEvent event) {
310        process(event);
311    }
312
313    public void processTransactionTerminated(TransactionTerminatedEvent event) {
314        process(event);
315    }
316
317    public void processDialogTerminated(DialogTerminatedEvent event) {
318        process(event);
319    }
320
321    private synchronized void process(EventObject event) {
322        SipSessionImpl session = getSipSession(event);
323        try {
324            boolean isLoggable = isLoggable(session, event);
325            boolean processed = (session != null) && session.process(event);
326            if (isLoggable && processed) {
327                Log.d(TAG, "new state after: "
328                        + SipSession.State.toString(session.mState));
329            }
330        } catch (Throwable e) {
331            Log.w(TAG, "event process error: " + event, e);
332            session.onError(e);
333        }
334    }
335
336    private String extractContent(Message message) {
337        // Currently we do not support secure MIME bodies.
338        byte[] bytes = message.getRawContent();
339        if (bytes != null) {
340            try {
341                if (message instanceof SIPMessage) {
342                    return ((SIPMessage) message).getMessageContent();
343                } else {
344                    return new String(bytes, "UTF-8");
345                }
346            } catch (UnsupportedEncodingException e) {
347            }
348        }
349        return null;
350    }
351
352    private class SipSessionCallReceiverImpl extends SipSessionImpl {
353        public SipSessionCallReceiverImpl(ISipSessionListener listener) {
354            super(listener);
355        }
356
357        public boolean process(EventObject evt) throws SipException {
358            if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~   " + this + ": "
359                    + SipSession.State.toString(mState) + ": processing "
360                    + log(evt));
361            if (isRequestEvent(Request.INVITE, evt)) {
362                RequestEvent event = (RequestEvent) evt;
363                SipSessionImpl newSession = new SipSessionImpl(mProxy);
364                newSession.mState = SipSession.State.INCOMING_CALL;
365                newSession.mServerTransaction = mSipHelper.sendRinging(event,
366                        generateTag());
367                newSession.mDialog = newSession.mServerTransaction.getDialog();
368                newSession.mInviteReceived = event;
369                newSession.mPeerProfile = createPeerProfile(event.getRequest());
370                newSession.mPeerSessionDescription =
371                        extractContent(event.getRequest());
372                addSipSession(newSession);
373                mProxy.onRinging(newSession, newSession.mPeerProfile,
374                        newSession.mPeerSessionDescription);
375                return true;
376            } else if (isRequestEvent(Request.OPTIONS, evt)) {
377                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
378                return true;
379            } else {
380                return false;
381            }
382        }
383    }
384
385    class SipSessionImpl extends ISipSession.Stub {
386        SipProfile mPeerProfile;
387        SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
388        int mState = SipSession.State.READY_TO_CALL;
389        RequestEvent mInviteReceived;
390        Dialog mDialog;
391        ServerTransaction mServerTransaction;
392        ClientTransaction mClientTransaction;
393        String mPeerSessionDescription;
394        boolean mInCall;
395        SessionTimer mTimer;
396        int mAuthenticationRetryCount;
397
398        // for registration
399        boolean mReRegisterFlag = false;
400        int mRPort;
401
402        // lightweight timer
403        class SessionTimer {
404            private boolean mRunning = true;
405
406            void start(final int timeout) {
407                new Thread(new Runnable() {
408                    public void run() {
409                        sleep(timeout);
410                        if (mRunning) timeout();
411                    }
412                }, "SipSessionTimerThread").start();
413            }
414
415            synchronized void cancel() {
416                mRunning = false;
417                this.notify();
418            }
419
420            private void timeout() {
421                synchronized (SipSessionGroup.this) {
422                    onError(SipErrorCode.TIME_OUT, "Session timed out!");
423                }
424            }
425
426            private synchronized void sleep(int timeout) {
427                try {
428                    this.wait(timeout * 1000);
429                } catch (InterruptedException e) {
430                    Log.e(TAG, "session timer interrupted!");
431                }
432            }
433        }
434
435        public SipSessionImpl(ISipSessionListener listener) {
436            setListener(listener);
437        }
438
439        SipSessionImpl duplicate() {
440            return new SipSessionImpl(mProxy.getListener());
441        }
442
443        private void reset() {
444            mInCall = false;
445            removeSipSession(this);
446            mPeerProfile = null;
447            mState = SipSession.State.READY_TO_CALL;
448            mInviteReceived = null;
449            mPeerSessionDescription = null;
450            mRPort = 0;
451            mAuthenticationRetryCount = 0;
452
453            if (mDialog != null) mDialog.delete();
454            mDialog = null;
455
456            try {
457                if (mServerTransaction != null) mServerTransaction.terminate();
458            } catch (ObjectInUseException e) {
459                // ignored
460            }
461            mServerTransaction = null;
462
463            try {
464                if (mClientTransaction != null) mClientTransaction.terminate();
465            } catch (ObjectInUseException e) {
466                // ignored
467            }
468            mClientTransaction = null;
469
470            cancelSessionTimer();
471        }
472
473        public boolean isInCall() {
474            return mInCall;
475        }
476
477        public String getLocalIp() {
478            return mLocalIp;
479        }
480
481        public SipProfile getLocalProfile() {
482            return mLocalProfile;
483        }
484
485        public SipProfile getPeerProfile() {
486            return mPeerProfile;
487        }
488
489        public String getCallId() {
490            return SipHelper.getCallId(getTransaction());
491        }
492
493        private Transaction getTransaction() {
494            if (mClientTransaction != null) return mClientTransaction;
495            if (mServerTransaction != null) return mServerTransaction;
496            return null;
497        }
498
499        public int getState() {
500            return mState;
501        }
502
503        public void setListener(ISipSessionListener listener) {
504            mProxy.setListener((listener instanceof SipSessionListenerProxy)
505                    ? ((SipSessionListenerProxy) listener).getListener()
506                    : listener);
507        }
508
509        // process the command in a new thread
510        private void doCommandAsync(final EventObject command) {
511            new Thread(new Runnable() {
512                    public void run() {
513                        try {
514                            processCommand(command);
515                        } catch (Throwable e) {
516                            Log.w(TAG, "command error: " + command, e);
517                            onError(e);
518                        }
519                    }
520            }, "SipSessionAsyncCmdThread").start();
521        }
522
523        public void makeCall(SipProfile peerProfile, String sessionDescription,
524                int timeout) {
525            doCommandAsync(new MakeCallCommand(peerProfile, sessionDescription,
526                    timeout));
527        }
528
529        public void answerCall(String sessionDescription, int timeout) {
530            try {
531                processCommand(new MakeCallCommand(mPeerProfile,
532                        sessionDescription, timeout));
533            } catch (SipException e) {
534                onError(e);
535            }
536        }
537
538        public void endCall() {
539            doCommandAsync(END_CALL);
540        }
541
542        public void changeCall(String sessionDescription, int timeout) {
543            doCommandAsync(new MakeCallCommand(mPeerProfile, sessionDescription,
544                    timeout));
545        }
546
547        public void changeCallWithTimeout(
548                String sessionDescription, int timeout) {
549            doCommandAsync(new MakeCallCommand(mPeerProfile, sessionDescription,
550                    timeout));
551        }
552
553        public void register(int duration) {
554            doCommandAsync(new RegisterCommand(duration));
555        }
556
557        public void unregister() {
558            doCommandAsync(DEREGISTER);
559        }
560
561        public boolean isReRegisterRequired() {
562            return mReRegisterFlag;
563        }
564
565        public void clearReRegisterRequired() {
566            mReRegisterFlag = false;
567        }
568
569        public void sendKeepAlive() {
570            mState = SipSession.State.PINGING;
571            try {
572                processCommand(new OptionsCommand());
573                for (int i = 0; i < 15; i++) {
574                    if (SipSession.State.PINGING != mState) break;
575                    Thread.sleep(200);
576                }
577                if (SipSession.State.PINGING == mState) {
578                    // FIXME: what to do if server doesn't respond
579                    reset();
580                    if (DEBUG) Log.w(TAG, "no response from ping");
581                }
582            } catch (SipException e) {
583                Log.e(TAG, "sendKeepAlive failed", e);
584            } catch (InterruptedException e) {
585                Log.e(TAG, "sendKeepAlive interrupted", e);
586            }
587        }
588
589        private void processCommand(EventObject command) throws SipException {
590            if (isLoggable(command)) Log.d(TAG, "process cmd: " + command);
591            if (!process(command)) {
592                onError(SipErrorCode.IN_PROGRESS,
593                        "cannot initiate a new transaction to execute: "
594                        + command);
595            }
596        }
597
598        protected String generateTag() {
599            // 32-bit randomness
600            return String.valueOf((long) (Math.random() * 0x100000000L));
601        }
602
603        public String toString() {
604            try {
605                String s = super.toString();
606                return s.substring(s.indexOf("@")) + ":"
607                        + SipSession.State.toString(mState);
608            } catch (Throwable e) {
609                return super.toString();
610            }
611        }
612
613        public boolean process(EventObject evt) throws SipException {
614            if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~   " + this + ": "
615                    + SipSession.State.toString(mState) + ": processing "
616                    + log(evt));
617            synchronized (SipSessionGroup.this) {
618                if (isClosed()) return false;
619
620                Dialog dialog = null;
621                if (evt instanceof RequestEvent) {
622                    dialog = ((RequestEvent) evt).getDialog();
623                } else if (evt instanceof ResponseEvent) {
624                    dialog = ((ResponseEvent) evt).getDialog();
625                }
626                if (dialog != null) mDialog = dialog;
627
628                boolean processed;
629
630                switch (mState) {
631                case SipSession.State.REGISTERING:
632                case SipSession.State.DEREGISTERING:
633                    processed = registeringToReady(evt);
634                    break;
635                case SipSession.State.PINGING:
636                    processed = keepAliveProcess(evt);
637                    break;
638                case SipSession.State.READY_TO_CALL:
639                    processed = readyForCall(evt);
640                    break;
641                case SipSession.State.INCOMING_CALL:
642                    processed = incomingCall(evt);
643                    break;
644                case SipSession.State.INCOMING_CALL_ANSWERING:
645                    processed = incomingCallToInCall(evt);
646                    break;
647                case SipSession.State.OUTGOING_CALL:
648                case SipSession.State.OUTGOING_CALL_RING_BACK:
649                    processed = outgoingCall(evt);
650                    break;
651                case SipSession.State.OUTGOING_CALL_CANCELING:
652                    processed = outgoingCallToReady(evt);
653                    break;
654                case SipSession.State.IN_CALL:
655                    processed = inCall(evt);
656                    break;
657                default:
658                    processed = false;
659                }
660                return (processed || processExceptions(evt));
661            }
662        }
663
664        private boolean processExceptions(EventObject evt) throws SipException {
665            if (isRequestEvent(Request.BYE, evt)) {
666                // terminate the call whenever a BYE is received
667                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
668                endCallNormally();
669                return true;
670            } else if (isRequestEvent(Request.CANCEL, evt)) {
671                mSipHelper.sendResponse((RequestEvent) evt,
672                        Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST);
673                return true;
674            } else if (evt instanceof TransactionTerminatedEvent) {
675                if (isCurrentTransaction((TransactionTerminatedEvent) evt)) {
676                    if (evt instanceof TimeoutEvent) {
677                        processTimeout((TimeoutEvent) evt);
678                    } else {
679                        processTransactionTerminated(
680                                (TransactionTerminatedEvent) evt);
681                    }
682                    return true;
683                }
684            } else if (isRequestEvent(Request.OPTIONS, evt)) {
685                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
686                return true;
687            } else if (evt instanceof DialogTerminatedEvent) {
688                processDialogTerminated((DialogTerminatedEvent) evt);
689                return true;
690            }
691            return false;
692        }
693
694        private void processDialogTerminated(DialogTerminatedEvent event) {
695            if (mDialog == event.getDialog()) {
696                onError(new SipException("dialog terminated"));
697            } else {
698                Log.d(TAG, "not the current dialog; current=" + mDialog
699                        + ", terminated=" + event.getDialog());
700            }
701        }
702
703        private boolean isCurrentTransaction(TransactionTerminatedEvent event) {
704            Transaction current = event.isServerTransaction()
705                    ? mServerTransaction
706                    : mClientTransaction;
707            Transaction target = event.isServerTransaction()
708                    ? event.getServerTransaction()
709                    : event.getClientTransaction();
710
711            if ((current != target) && (mState != SipSession.State.PINGING)) {
712                Log.d(TAG, "not the current transaction; current="
713                        + toString(current) + ", target=" + toString(target));
714                return false;
715            } else if (current != null) {
716                Log.d(TAG, "transaction terminated: " + toString(current));
717                return true;
718            } else {
719                // no transaction; shouldn't be here; ignored
720                return true;
721            }
722        }
723
724        private String toString(Transaction transaction) {
725            if (transaction == null) return "null";
726            Request request = transaction.getRequest();
727            Dialog dialog = transaction.getDialog();
728            CSeqHeader cseq = (CSeqHeader) request.getHeader(CSeqHeader.NAME);
729            return String.format("req=%s,%s,s=%s,ds=%s,", request.getMethod(),
730                    cseq.getSeqNumber(), transaction.getState(),
731                    ((dialog == null) ? "-" : dialog.getState()));
732        }
733
734        private void processTransactionTerminated(
735                TransactionTerminatedEvent event) {
736            switch (mState) {
737                case SipSession.State.IN_CALL:
738                case SipSession.State.READY_TO_CALL:
739                    Log.d(TAG, "Transaction terminated; do nothing");
740                    break;
741                default:
742                    Log.d(TAG, "Transaction terminated early: " + this);
743                    onError(SipErrorCode.TRANSACTION_TERMINTED,
744                            "transaction terminated");
745            }
746        }
747
748        private void processTimeout(TimeoutEvent event) {
749            Log.d(TAG, "processing Timeout...");
750            switch (mState) {
751                case SipSession.State.REGISTERING:
752                case SipSession.State.DEREGISTERING:
753                    reset();
754                    mProxy.onRegistrationTimeout(this);
755                    break;
756                case SipSession.State.INCOMING_CALL:
757                case SipSession.State.INCOMING_CALL_ANSWERING:
758                case SipSession.State.OUTGOING_CALL:
759                case SipSession.State.OUTGOING_CALL_CANCELING:
760                    onError(SipErrorCode.TIME_OUT, event.toString());
761                    break;
762                case SipSession.State.PINGING:
763                    reset();
764                    mReRegisterFlag = true;
765                    break;
766
767                default:
768                    Log.d(TAG, "   do nothing");
769                    break;
770            }
771        }
772
773        private int getExpiryTime(Response response) {
774            int expires = EXPIRY_TIME;
775            ExpiresHeader expiresHeader = (ExpiresHeader)
776                    response.getHeader(ExpiresHeader.NAME);
777            if (expiresHeader != null) expires = expiresHeader.getExpires();
778            expiresHeader = (ExpiresHeader)
779                    response.getHeader(MinExpiresHeader.NAME);
780            if (expiresHeader != null) {
781                expires = Math.max(expires, expiresHeader.getExpires());
782            }
783            return expires;
784        }
785
786        private boolean keepAliveProcess(EventObject evt) throws SipException {
787            if (evt instanceof OptionsCommand) {
788                mClientTransaction = mSipHelper.sendKeepAlive(mLocalProfile,
789                        generateTag());
790                mDialog = mClientTransaction.getDialog();
791                addSipSession(this);
792                return true;
793            } else if (evt instanceof ResponseEvent) {
794                return parseOptionsResult(evt);
795            }
796            return false;
797        }
798
799        private boolean parseOptionsResult(EventObject evt) {
800            if (expectResponse(Request.OPTIONS, evt)) {
801                ResponseEvent event = (ResponseEvent) evt;
802                int rPort = getRPortFromResponse(event.getResponse());
803                if (rPort != -1) {
804                    if (mRPort == 0) mRPort = rPort;
805                    if (mRPort != rPort) {
806                        mReRegisterFlag = true;
807                        if (DEBUG) Log.w(TAG, String.format(
808                                "rport is changed: %d <> %d", mRPort, rPort));
809                        mRPort = rPort;
810                    } else {
811                        if (DEBUG_PING) Log.w(TAG, "rport is the same: " + rPort);
812                    }
813                } else {
814                    if (DEBUG) Log.w(TAG, "peer did not respond rport");
815                }
816                reset();
817                return true;
818            }
819            return false;
820        }
821
822        private int getRPortFromResponse(Response response) {
823            ViaHeader viaHeader = (ViaHeader)(response.getHeader(
824                    SIPHeaderNames.VIA));
825            return (viaHeader == null) ? -1 : viaHeader.getRPort();
826        }
827
828        private boolean registeringToReady(EventObject evt)
829                throws SipException {
830            if (expectResponse(Request.REGISTER, evt)) {
831                ResponseEvent event = (ResponseEvent) evt;
832                Response response = event.getResponse();
833
834                int statusCode = response.getStatusCode();
835                switch (statusCode) {
836                case Response.OK:
837                    int state = mState;
838                    onRegistrationDone((state == SipSession.State.REGISTERING)
839                            ? getExpiryTime(((ResponseEvent) evt).getResponse())
840                            : -1);
841                    return true;
842                case Response.UNAUTHORIZED:
843                case Response.PROXY_AUTHENTICATION_REQUIRED:
844                    handleAuthentication(event);
845                    return true;
846                default:
847                    if (statusCode >= 500) {
848                        onRegistrationFailed(response);
849                        return true;
850                    }
851                }
852            }
853            return false;
854        }
855
856        private boolean handleAuthentication(ResponseEvent event)
857                throws SipException {
858            Response response = event.getResponse();
859            String nonce = getNonceFromResponse(response);
860            if (nonce == null) {
861                onError(SipErrorCode.SERVER_ERROR,
862                        "server does not provide challenge");
863                return false;
864            } else if (mAuthenticationRetryCount < 2) {
865                mClientTransaction = mSipHelper.handleChallenge(
866                        event, getAccountManager());
867                mDialog = mClientTransaction.getDialog();
868                mAuthenticationRetryCount++;
869                if (isLoggable(this, event)) {
870                    Log.d(TAG, "   authentication retry count="
871                            + mAuthenticationRetryCount);
872                }
873                return true;
874            } else {
875                onError(SipErrorCode.INVALID_CREDENTIALS,
876                        "incorrect username or password");
877                return false;
878            }
879        }
880
881        private boolean crossDomainAuthenticationRequired(Response response) {
882            String realm = getRealmFromResponse(response);
883            if (realm == null) realm = "";
884            return !mLocalProfile.getSipDomain().trim().equals(realm.trim());
885        }
886
887        private AccountManager getAccountManager() {
888            return new AccountManager() {
889                public UserCredentials getCredentials(ClientTransaction
890                        challengedTransaction, String realm) {
891                    return new UserCredentials() {
892                        public String getUserName() {
893                            return mLocalProfile.getUserName();
894                        }
895
896                        public String getPassword() {
897                            return mPassword;
898                        }
899
900                        public String getSipDomain() {
901                            return mLocalProfile.getSipDomain();
902                        }
903                    };
904                }
905            };
906        }
907
908        private String getRealmFromResponse(Response response) {
909            WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
910                    SIPHeaderNames.WWW_AUTHENTICATE);
911            if (wwwAuth != null) return wwwAuth.getRealm();
912            ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
913                    SIPHeaderNames.PROXY_AUTHENTICATE);
914            return (proxyAuth == null) ? null : proxyAuth.getRealm();
915        }
916
917        private String getNonceFromResponse(Response response) {
918            WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
919                    SIPHeaderNames.WWW_AUTHENTICATE);
920            if (wwwAuth != null) return wwwAuth.getNonce();
921            ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
922                    SIPHeaderNames.PROXY_AUTHENTICATE);
923            return (proxyAuth == null) ? null : proxyAuth.getNonce();
924        }
925
926        private boolean readyForCall(EventObject evt) throws SipException {
927            // expect MakeCallCommand, RegisterCommand, DEREGISTER
928            if (evt instanceof MakeCallCommand) {
929                mState = SipSession.State.OUTGOING_CALL;
930                MakeCallCommand cmd = (MakeCallCommand) evt;
931                mPeerProfile = cmd.getPeerProfile();
932                mClientTransaction = mSipHelper.sendInvite(mLocalProfile,
933                        mPeerProfile, cmd.getSessionDescription(),
934                        generateTag());
935                mDialog = mClientTransaction.getDialog();
936                addSipSession(this);
937                startSessionTimer(cmd.getTimeout());
938                mProxy.onCalling(this);
939                return true;
940            } else if (evt instanceof RegisterCommand) {
941                mState = SipSession.State.REGISTERING;
942                int duration = ((RegisterCommand) evt).getDuration();
943                mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
944                        generateTag(), duration);
945                mDialog = mClientTransaction.getDialog();
946                addSipSession(this);
947                mProxy.onRegistering(this);
948                return true;
949            } else if (DEREGISTER == evt) {
950                mState = SipSession.State.DEREGISTERING;
951                mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
952                        generateTag(), 0);
953                mDialog = mClientTransaction.getDialog();
954                addSipSession(this);
955                mProxy.onRegistering(this);
956                return true;
957            }
958            return false;
959        }
960
961        private boolean incomingCall(EventObject evt) throws SipException {
962            // expect MakeCallCommand(answering) , END_CALL cmd , Cancel
963            if (evt instanceof MakeCallCommand) {
964                // answer call
965                mState = SipSession.State.INCOMING_CALL_ANSWERING;
966                mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived,
967                        mLocalProfile,
968                        ((MakeCallCommand) evt).getSessionDescription(),
969                        mServerTransaction);
970                startSessionTimer(((MakeCallCommand) evt).getTimeout());
971                return true;
972            } else if (END_CALL == evt) {
973                mSipHelper.sendInviteBusyHere(mInviteReceived,
974                        mServerTransaction);
975                endCallNormally();
976                return true;
977            } else if (isRequestEvent(Request.CANCEL, evt)) {
978                RequestEvent event = (RequestEvent) evt;
979                mSipHelper.sendResponse(event, Response.OK);
980                mSipHelper.sendInviteRequestTerminated(
981                        mInviteReceived.getRequest(), mServerTransaction);
982                endCallNormally();
983                return true;
984            }
985            return false;
986        }
987
988        private boolean incomingCallToInCall(EventObject evt)
989                throws SipException {
990            // expect ACK, CANCEL request
991            if (isRequestEvent(Request.ACK, evt)) {
992                establishCall();
993                return true;
994            } else if (isRequestEvent(Request.CANCEL, evt)) {
995                // http://tools.ietf.org/html/rfc3261#section-9.2
996                // Final response has been sent; do nothing here.
997                return true;
998            }
999            return false;
1000        }
1001
1002        private boolean outgoingCall(EventObject evt) throws SipException {
1003            if (expectResponse(Request.INVITE, evt)) {
1004                ResponseEvent event = (ResponseEvent) evt;
1005                Response response = event.getResponse();
1006
1007                int statusCode = response.getStatusCode();
1008                switch (statusCode) {
1009                case Response.RINGING:
1010                case Response.CALL_IS_BEING_FORWARDED:
1011                case Response.QUEUED:
1012                case Response.SESSION_PROGRESS:
1013                    // feedback any provisional responses (except TRYING) as
1014                    // ring back for better UX
1015                    if (mState == SipSession.State.OUTGOING_CALL) {
1016                        mState = SipSession.State.OUTGOING_CALL_RING_BACK;
1017                        cancelSessionTimer();
1018                        mProxy.onRingingBack(this);
1019                    }
1020                    return true;
1021                case Response.OK:
1022                    mSipHelper.sendInviteAck(event, mDialog);
1023                    mPeerSessionDescription = extractContent(response);
1024                    establishCall();
1025                    return true;
1026                case Response.UNAUTHORIZED:
1027                case Response.PROXY_AUTHENTICATION_REQUIRED:
1028                    if (crossDomainAuthenticationRequired(response)) {
1029                        onError(SipErrorCode.CROSS_DOMAIN_AUTHENTICATION,
1030                                getRealmFromResponse(response));
1031                    } else if (handleAuthentication(event)) {
1032                        addSipSession(this);
1033                    }
1034                    return true;
1035                case Response.REQUEST_PENDING:
1036                    // TODO:
1037                    // rfc3261#section-14.1; re-schedule invite
1038                    return true;
1039                default:
1040                    if (statusCode >= 400) {
1041                        // error: an ack is sent automatically by the stack
1042                        onError(response);
1043                        return true;
1044                    } else if (statusCode >= 300) {
1045                        // TODO: handle 3xx (redirect)
1046                    } else {
1047                        return true;
1048                    }
1049                }
1050                return false;
1051            } else if (END_CALL == evt) {
1052                // RFC says that UA should not send out cancel when no
1053                // response comes back yet. We are cheating for not checking
1054                // response.
1055                mState = SipSession.State.OUTGOING_CALL_CANCELING;
1056                mSipHelper.sendCancel(mClientTransaction);
1057                startSessionTimer(CANCEL_CALL_TIMER);
1058                return true;
1059            } else if (isRequestEvent(Request.INVITE, evt)) {
1060                // Call self? Send BUSY HERE so server may redirect the call to
1061                // voice mailbox.
1062                RequestEvent event = (RequestEvent) evt;
1063                mSipHelper.sendInviteBusyHere(event,
1064                        event.getServerTransaction());
1065                return true;
1066            }
1067            return false;
1068        }
1069
1070        private boolean outgoingCallToReady(EventObject evt)
1071                throws SipException {
1072            if (evt instanceof ResponseEvent) {
1073                ResponseEvent event = (ResponseEvent) evt;
1074                Response response = event.getResponse();
1075                int statusCode = response.getStatusCode();
1076                if (expectResponse(Request.CANCEL, evt)) {
1077                    if (statusCode == Response.OK) {
1078                        // do nothing; wait for REQUEST_TERMINATED
1079                        return true;
1080                    }
1081                } else if (expectResponse(Request.INVITE, evt)) {
1082                    switch (statusCode) {
1083                        case Response.OK:
1084                            outgoingCall(evt); // abort Cancel
1085                            return true;
1086                        case Response.REQUEST_TERMINATED:
1087                            endCallNormally();
1088                            return true;
1089                    }
1090                } else {
1091                    return false;
1092                }
1093
1094                if (statusCode >= 400) {
1095                    onError(response);
1096                    return true;
1097                }
1098            } else if (evt instanceof TransactionTerminatedEvent) {
1099                // rfc3261#section-14.1:
1100                // if re-invite gets timed out, terminate the dialog; but
1101                // re-invite is not reliable, just let it go and pretend
1102                // nothing happened.
1103                onError(new SipException("timed out"));
1104            }
1105            return false;
1106        }
1107
1108        private boolean inCall(EventObject evt) throws SipException {
1109            // expect END_CALL cmd, BYE request, hold call (MakeCallCommand)
1110            // OK retransmission is handled in SipStack
1111            if (END_CALL == evt) {
1112                // rfc3261#section-15.1.1
1113                mSipHelper.sendBye(mDialog);
1114                endCallNormally();
1115                return true;
1116            } else if (isRequestEvent(Request.INVITE, evt)) {
1117                // got Re-INVITE
1118                mState = SipSession.State.INCOMING_CALL;
1119                RequestEvent event = mInviteReceived = (RequestEvent) evt;
1120                mPeerSessionDescription = extractContent(event.getRequest());
1121                mServerTransaction = null;
1122                mProxy.onRinging(this, mPeerProfile, mPeerSessionDescription);
1123                return true;
1124            } else if (isRequestEvent(Request.BYE, evt)) {
1125                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
1126                endCallNormally();
1127                return true;
1128            } else if (evt instanceof MakeCallCommand) {
1129                // to change call
1130                mState = SipSession.State.OUTGOING_CALL;
1131                mClientTransaction = mSipHelper.sendReinvite(mDialog,
1132                        ((MakeCallCommand) evt).getSessionDescription());
1133                startSessionTimer(((MakeCallCommand) evt).getTimeout());
1134                return true;
1135            }
1136            return false;
1137        }
1138
1139        // timeout in seconds
1140        private void startSessionTimer(int timeout) {
1141            if (timeout > 0) {
1142                mTimer = new SessionTimer();
1143                mTimer.start(timeout);
1144            }
1145        }
1146
1147        private void cancelSessionTimer() {
1148            if (mTimer != null) {
1149                mTimer.cancel();
1150                mTimer = null;
1151            }
1152        }
1153
1154        private String createErrorMessage(Response response) {
1155            return String.format("%s (%d)", response.getReasonPhrase(),
1156                    response.getStatusCode());
1157        }
1158
1159        private void establishCall() {
1160            mState = SipSession.State.IN_CALL;
1161            mInCall = true;
1162            cancelSessionTimer();
1163            mProxy.onCallEstablished(this, mPeerSessionDescription);
1164        }
1165
1166        private void endCallNormally() {
1167            reset();
1168            mProxy.onCallEnded(this);
1169        }
1170
1171        private void endCallOnError(int errorCode, String message) {
1172            reset();
1173            mProxy.onError(this, errorCode, message);
1174        }
1175
1176        private void endCallOnBusy() {
1177            reset();
1178            mProxy.onCallBusy(this);
1179        }
1180
1181        private void onError(int errorCode, String message) {
1182            cancelSessionTimer();
1183            switch (mState) {
1184                case SipSession.State.REGISTERING:
1185                case SipSession.State.DEREGISTERING:
1186                    onRegistrationFailed(errorCode, message);
1187                    break;
1188                default:
1189                    endCallOnError(errorCode, message);
1190            }
1191        }
1192
1193
1194        private void onError(Throwable exception) {
1195            exception = getRootCause(exception);
1196            onError(getErrorCode(exception), exception.toString());
1197        }
1198
1199        private void onError(Response response) {
1200            int statusCode = response.getStatusCode();
1201            if (!mInCall && (statusCode == Response.BUSY_HERE)) {
1202                endCallOnBusy();
1203            } else {
1204                onError(getErrorCode(statusCode), createErrorMessage(response));
1205            }
1206        }
1207
1208        private int getErrorCode(int responseStatusCode) {
1209            switch (responseStatusCode) {
1210                case Response.TEMPORARILY_UNAVAILABLE:
1211                case Response.FORBIDDEN:
1212                case Response.GONE:
1213                case Response.NOT_FOUND:
1214                case Response.NOT_ACCEPTABLE:
1215                case Response.NOT_ACCEPTABLE_HERE:
1216                    return SipErrorCode.PEER_NOT_REACHABLE;
1217
1218                case Response.REQUEST_URI_TOO_LONG:
1219                case Response.ADDRESS_INCOMPLETE:
1220                case Response.AMBIGUOUS:
1221                    return SipErrorCode.INVALID_REMOTE_URI;
1222
1223                case Response.REQUEST_TIMEOUT:
1224                    return SipErrorCode.TIME_OUT;
1225
1226                default:
1227                    if (responseStatusCode < 500) {
1228                        return SipErrorCode.CLIENT_ERROR;
1229                    } else {
1230                        return SipErrorCode.SERVER_ERROR;
1231                    }
1232            }
1233        }
1234
1235        private Throwable getRootCause(Throwable exception) {
1236            Throwable cause = exception.getCause();
1237            while (cause != null) {
1238                exception = cause;
1239                cause = exception.getCause();
1240            }
1241            return exception;
1242        }
1243
1244        private int getErrorCode(Throwable exception) {
1245            String message = exception.getMessage();
1246            if (exception instanceof UnknownHostException) {
1247                return SipErrorCode.SERVER_UNREACHABLE;
1248            } else if (exception instanceof IOException) {
1249                return SipErrorCode.SOCKET_ERROR;
1250            } else {
1251                return SipErrorCode.CLIENT_ERROR;
1252            }
1253        }
1254
1255        private void onRegistrationDone(int duration) {
1256            reset();
1257            mProxy.onRegistrationDone(this, duration);
1258        }
1259
1260        private void onRegistrationFailed(int errorCode, String message) {
1261            reset();
1262            mProxy.onRegistrationFailed(this, errorCode, message);
1263        }
1264
1265        private void onRegistrationFailed(Throwable exception) {
1266            exception = getRootCause(exception);
1267            onRegistrationFailed(getErrorCode(exception),
1268                    exception.toString());
1269        }
1270
1271        private void onRegistrationFailed(Response response) {
1272            int statusCode = response.getStatusCode();
1273            onRegistrationFailed(getErrorCode(statusCode),
1274                    createErrorMessage(response));
1275        }
1276    }
1277
1278    /**
1279     * @return true if the event is a request event matching the specified
1280     *      method; false otherwise
1281     */
1282    private static boolean isRequestEvent(String method, EventObject event) {
1283        try {
1284            if (event instanceof RequestEvent) {
1285                RequestEvent requestEvent = (RequestEvent) event;
1286                return method.equals(requestEvent.getRequest().getMethod());
1287            }
1288        } catch (Throwable e) {
1289        }
1290        return false;
1291    }
1292
1293    private static String getCseqMethod(Message message) {
1294        return ((CSeqHeader) message.getHeader(CSeqHeader.NAME)).getMethod();
1295    }
1296
1297    /**
1298     * @return true if the event is a response event and the CSeqHeader method
1299     * match the given arguments; false otherwise
1300     */
1301    private static boolean expectResponse(
1302            String expectedMethod, EventObject evt) {
1303        if (evt instanceof ResponseEvent) {
1304            ResponseEvent event = (ResponseEvent) evt;
1305            Response response = event.getResponse();
1306            return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
1307        }
1308        return false;
1309    }
1310
1311    /**
1312     * @return true if the event is a response event and the response code and
1313     *      CSeqHeader method match the given arguments; false otherwise
1314     */
1315    private static boolean expectResponse(
1316            int responseCode, String expectedMethod, EventObject evt) {
1317        if (evt instanceof ResponseEvent) {
1318            ResponseEvent event = (ResponseEvent) evt;
1319            Response response = event.getResponse();
1320            if (response.getStatusCode() == responseCode) {
1321                return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
1322            }
1323        }
1324        return false;
1325    }
1326
1327    private static SipProfile createPeerProfile(Request request)
1328            throws SipException {
1329        try {
1330            FromHeader fromHeader =
1331                    (FromHeader) request.getHeader(FromHeader.NAME);
1332            Address address = fromHeader.getAddress();
1333            SipURI uri = (SipURI) address.getURI();
1334            String username = uri.getUser();
1335            if (username == null) username = ANONYMOUS;
1336            return new SipProfile.Builder(username, uri.getHost())
1337                    .setPort(uri.getPort())
1338                    .setDisplayName(address.getDisplayName())
1339                    .build();
1340        } catch (IllegalArgumentException e) {
1341            throw new SipException("createPeerProfile()", e);
1342        } catch (ParseException e) {
1343            throw new SipException("createPeerProfile()", e);
1344        }
1345    }
1346
1347    private static boolean isLoggable(SipSessionImpl s) {
1348        if (s != null) {
1349            switch (s.mState) {
1350                case SipSession.State.PINGING:
1351                    return DEBUG_PING;
1352            }
1353        }
1354        return DEBUG;
1355    }
1356
1357    private static boolean isLoggable(EventObject evt) {
1358        return isLoggable(null, evt);
1359    }
1360
1361    private static boolean isLoggable(SipSessionImpl s, EventObject evt) {
1362        if (!isLoggable(s)) return false;
1363        if (evt == null) return false;
1364
1365        if (evt instanceof OptionsCommand) {
1366            return DEBUG_PING;
1367        } else if (evt instanceof ResponseEvent) {
1368            Response response = ((ResponseEvent) evt).getResponse();
1369            if (Request.OPTIONS.equals(response.getHeader(CSeqHeader.NAME))) {
1370                return DEBUG_PING;
1371            }
1372            return DEBUG;
1373        } else if (evt instanceof RequestEvent) {
1374            return DEBUG;
1375        }
1376        return false;
1377    }
1378
1379    private static String log(EventObject evt) {
1380        if (evt instanceof RequestEvent) {
1381            return ((RequestEvent) evt).getRequest().toString();
1382        } else if (evt instanceof ResponseEvent) {
1383            return ((ResponseEvent) evt).getResponse().toString();
1384        } else {
1385            return evt.toString();
1386        }
1387    }
1388
1389    private class OptionsCommand extends EventObject {
1390        public OptionsCommand() {
1391            super(SipSessionGroup.this);
1392        }
1393    }
1394
1395    private class RegisterCommand extends EventObject {
1396        private int mDuration;
1397
1398        public RegisterCommand(int duration) {
1399            super(SipSessionGroup.this);
1400            mDuration = duration;
1401        }
1402
1403        public int getDuration() {
1404            return mDuration;
1405        }
1406    }
1407
1408    private class MakeCallCommand extends EventObject {
1409        private String mSessionDescription;
1410        private int mTimeout; // in seconds
1411
1412        public MakeCallCommand(SipProfile peerProfile,
1413                String sessionDescription) {
1414            this(peerProfile, sessionDescription, -1);
1415        }
1416
1417        public MakeCallCommand(SipProfile peerProfile,
1418                String sessionDescription, int timeout) {
1419            super(peerProfile);
1420            mSessionDescription = sessionDescription;
1421            mTimeout = timeout;
1422        }
1423
1424        public SipProfile getPeerProfile() {
1425            return (SipProfile) getSource();
1426        }
1427
1428        public String getSessionDescription() {
1429            return mSessionDescription;
1430        }
1431
1432        public int getTimeout() {
1433            return mTimeout;
1434        }
1435    }
1436}
1437