SipSessionGroup.java revision c133781723f64d1321685d02ad6a208286bf0a42
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 = 0;
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            mAuthenticationRetryCount = 0;
451
452            if (mDialog != null) mDialog.delete();
453            mDialog = null;
454
455            try {
456                if (mServerTransaction != null) mServerTransaction.terminate();
457            } catch (ObjectInUseException e) {
458                // ignored
459            }
460            mServerTransaction = null;
461
462            try {
463                if (mClientTransaction != null) mClientTransaction.terminate();
464            } catch (ObjectInUseException e) {
465                // ignored
466            }
467            mClientTransaction = null;
468
469            cancelSessionTimer();
470        }
471
472        public boolean isInCall() {
473            return mInCall;
474        }
475
476        public String getLocalIp() {
477            return mLocalIp;
478        }
479
480        public SipProfile getLocalProfile() {
481            return mLocalProfile;
482        }
483
484        public SipProfile getPeerProfile() {
485            return mPeerProfile;
486        }
487
488        public String getCallId() {
489            return SipHelper.getCallId(getTransaction());
490        }
491
492        private Transaction getTransaction() {
493            if (mClientTransaction != null) return mClientTransaction;
494            if (mServerTransaction != null) return mServerTransaction;
495            return null;
496        }
497
498        public int getState() {
499            return mState;
500        }
501
502        public void setListener(ISipSessionListener listener) {
503            mProxy.setListener((listener instanceof SipSessionListenerProxy)
504                    ? ((SipSessionListenerProxy) listener).getListener()
505                    : listener);
506        }
507
508        // process the command in a new thread
509        private void doCommandAsync(final EventObject command) {
510            new Thread(new Runnable() {
511                    public void run() {
512                        try {
513                            processCommand(command);
514                        } catch (Throwable e) {
515                            Log.w(TAG, "command error: " + command, e);
516                            onError(e);
517                        }
518                    }
519            }, "SipSessionAsyncCmdThread").start();
520        }
521
522        public void makeCall(SipProfile peerProfile, String sessionDescription,
523                int timeout) {
524            doCommandAsync(new MakeCallCommand(peerProfile, sessionDescription,
525                    timeout));
526        }
527
528        public void answerCall(String sessionDescription, int timeout) {
529            synchronized (SipSessionGroup.this) {
530                if (mPeerProfile == null) return;
531                doCommandAsync(new MakeCallCommand(mPeerProfile,
532                        sessionDescription, timeout));
533            }
534        }
535
536        public void endCall() {
537            doCommandAsync(END_CALL);
538        }
539
540        public void changeCall(String sessionDescription, int timeout) {
541            synchronized (SipSessionGroup.this) {
542                if (mPeerProfile == null) return;
543                doCommandAsync(new MakeCallCommand(mPeerProfile,
544                        sessionDescription, timeout));
545            }
546        }
547
548        public void register(int duration) {
549            doCommandAsync(new RegisterCommand(duration));
550        }
551
552        public void unregister() {
553            doCommandAsync(DEREGISTER);
554        }
555
556        public boolean isReRegisterRequired() {
557            return mReRegisterFlag;
558        }
559
560        public void clearReRegisterRequired() {
561            mReRegisterFlag = false;
562        }
563
564        public void sendKeepAlive() {
565            mState = SipSession.State.PINGING;
566            try {
567                processCommand(new OptionsCommand());
568                for (int i = 0; i < 15; i++) {
569                    if (SipSession.State.PINGING != mState) break;
570                    Thread.sleep(200);
571                }
572                if (SipSession.State.PINGING == mState) {
573                    // FIXME: what to do if server doesn't respond
574                    reset();
575                    if (DEBUG) Log.w(TAG, "no response from ping");
576                }
577            } catch (SipException e) {
578                Log.e(TAG, "sendKeepAlive failed", e);
579            } catch (InterruptedException e) {
580                Log.e(TAG, "sendKeepAlive interrupted", e);
581            }
582        }
583
584        private void processCommand(EventObject command) throws SipException {
585            if (isLoggable(command)) Log.d(TAG, "process cmd: " + command);
586            if (!process(command)) {
587                onError(SipErrorCode.IN_PROGRESS,
588                        "cannot initiate a new transaction to execute: "
589                        + command);
590            }
591        }
592
593        protected String generateTag() {
594            // 32-bit randomness
595            return String.valueOf((long) (Math.random() * 0x100000000L));
596        }
597
598        public String toString() {
599            try {
600                String s = super.toString();
601                return s.substring(s.indexOf("@")) + ":"
602                        + SipSession.State.toString(mState);
603            } catch (Throwable e) {
604                return super.toString();
605            }
606        }
607
608        public boolean process(EventObject evt) throws SipException {
609            if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~   " + this + ": "
610                    + SipSession.State.toString(mState) + ": processing "
611                    + log(evt));
612            synchronized (SipSessionGroup.this) {
613                if (isClosed()) return false;
614
615                Dialog dialog = null;
616                if (evt instanceof RequestEvent) {
617                    dialog = ((RequestEvent) evt).getDialog();
618                } else if (evt instanceof ResponseEvent) {
619                    dialog = ((ResponseEvent) evt).getDialog();
620                }
621                if (dialog != null) mDialog = dialog;
622
623                boolean processed;
624
625                switch (mState) {
626                case SipSession.State.REGISTERING:
627                case SipSession.State.DEREGISTERING:
628                    processed = registeringToReady(evt);
629                    break;
630                case SipSession.State.PINGING:
631                    processed = keepAliveProcess(evt);
632                    break;
633                case SipSession.State.READY_TO_CALL:
634                    processed = readyForCall(evt);
635                    break;
636                case SipSession.State.INCOMING_CALL:
637                    processed = incomingCall(evt);
638                    break;
639                case SipSession.State.INCOMING_CALL_ANSWERING:
640                    processed = incomingCallToInCall(evt);
641                    break;
642                case SipSession.State.OUTGOING_CALL:
643                case SipSession.State.OUTGOING_CALL_RING_BACK:
644                    processed = outgoingCall(evt);
645                    break;
646                case SipSession.State.OUTGOING_CALL_CANCELING:
647                    processed = outgoingCallToReady(evt);
648                    break;
649                case SipSession.State.IN_CALL:
650                    processed = inCall(evt);
651                    break;
652                default:
653                    processed = false;
654                }
655                return (processed || processExceptions(evt));
656            }
657        }
658
659        private boolean processExceptions(EventObject evt) throws SipException {
660            if (isRequestEvent(Request.BYE, evt)) {
661                // terminate the call whenever a BYE is received
662                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
663                endCallNormally();
664                return true;
665            } else if (isRequestEvent(Request.CANCEL, evt)) {
666                mSipHelper.sendResponse((RequestEvent) evt,
667                        Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST);
668                return true;
669            } else if (evt instanceof TransactionTerminatedEvent) {
670                if (isCurrentTransaction((TransactionTerminatedEvent) evt)) {
671                    if (evt instanceof TimeoutEvent) {
672                        processTimeout((TimeoutEvent) evt);
673                    } else {
674                        processTransactionTerminated(
675                                (TransactionTerminatedEvent) evt);
676                    }
677                    return true;
678                }
679            } else if (isRequestEvent(Request.OPTIONS, evt)) {
680                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
681                return true;
682            } else if (evt instanceof DialogTerminatedEvent) {
683                processDialogTerminated((DialogTerminatedEvent) evt);
684                return true;
685            }
686            return false;
687        }
688
689        private void processDialogTerminated(DialogTerminatedEvent event) {
690            if (mDialog == event.getDialog()) {
691                onError(new SipException("dialog terminated"));
692            } else {
693                Log.d(TAG, "not the current dialog; current=" + mDialog
694                        + ", terminated=" + event.getDialog());
695            }
696        }
697
698        private boolean isCurrentTransaction(TransactionTerminatedEvent event) {
699            Transaction current = event.isServerTransaction()
700                    ? mServerTransaction
701                    : mClientTransaction;
702            Transaction target = event.isServerTransaction()
703                    ? event.getServerTransaction()
704                    : event.getClientTransaction();
705
706            if ((current != target) && (mState != SipSession.State.PINGING)) {
707                Log.d(TAG, "not the current transaction; current="
708                        + toString(current) + ", target=" + toString(target));
709                return false;
710            } else if (current != null) {
711                Log.d(TAG, "transaction terminated: " + toString(current));
712                return true;
713            } else {
714                // no transaction; shouldn't be here; ignored
715                return true;
716            }
717        }
718
719        private String toString(Transaction transaction) {
720            if (transaction == null) return "null";
721            Request request = transaction.getRequest();
722            Dialog dialog = transaction.getDialog();
723            CSeqHeader cseq = (CSeqHeader) request.getHeader(CSeqHeader.NAME);
724            return String.format("req=%s,%s,s=%s,ds=%s,", request.getMethod(),
725                    cseq.getSeqNumber(), transaction.getState(),
726                    ((dialog == null) ? "-" : dialog.getState()));
727        }
728
729        private void processTransactionTerminated(
730                TransactionTerminatedEvent event) {
731            switch (mState) {
732                case SipSession.State.IN_CALL:
733                case SipSession.State.READY_TO_CALL:
734                    Log.d(TAG, "Transaction terminated; do nothing");
735                    break;
736                default:
737                    Log.d(TAG, "Transaction terminated early: " + this);
738                    onError(SipErrorCode.TRANSACTION_TERMINTED,
739                            "transaction terminated");
740            }
741        }
742
743        private void processTimeout(TimeoutEvent event) {
744            Log.d(TAG, "processing Timeout...");
745            switch (mState) {
746                case SipSession.State.REGISTERING:
747                case SipSession.State.DEREGISTERING:
748                    reset();
749                    mProxy.onRegistrationTimeout(this);
750                    break;
751                case SipSession.State.INCOMING_CALL:
752                case SipSession.State.INCOMING_CALL_ANSWERING:
753                case SipSession.State.OUTGOING_CALL:
754                case SipSession.State.OUTGOING_CALL_CANCELING:
755                    onError(SipErrorCode.TIME_OUT, event.toString());
756                    break;
757                case SipSession.State.PINGING:
758                    reset();
759                    mReRegisterFlag = true;
760                    break;
761
762                default:
763                    Log.d(TAG, "   do nothing");
764                    break;
765            }
766        }
767
768        private int getExpiryTime(Response response) {
769            int expires = EXPIRY_TIME;
770            ExpiresHeader expiresHeader = (ExpiresHeader)
771                    response.getHeader(ExpiresHeader.NAME);
772            if (expiresHeader != null) expires = expiresHeader.getExpires();
773            expiresHeader = (ExpiresHeader)
774                    response.getHeader(MinExpiresHeader.NAME);
775            if (expiresHeader != null) {
776                expires = Math.max(expires, expiresHeader.getExpires());
777            }
778            return expires;
779        }
780
781        private boolean keepAliveProcess(EventObject evt) throws SipException {
782            if (evt instanceof OptionsCommand) {
783                mClientTransaction = mSipHelper.sendKeepAlive(mLocalProfile,
784                        generateTag());
785                mDialog = mClientTransaction.getDialog();
786                addSipSession(this);
787                return true;
788            } else if (evt instanceof ResponseEvent) {
789                return parseOptionsResult(evt);
790            }
791            return false;
792        }
793
794        private boolean parseOptionsResult(EventObject evt) {
795            if (expectResponse(Request.OPTIONS, evt)) {
796                ResponseEvent event = (ResponseEvent) evt;
797                int rPort = getRPortFromResponse(event.getResponse());
798                if (rPort != -1) {
799                    if (mRPort == 0) mRPort = rPort;
800                    if (mRPort != rPort) {
801                        mReRegisterFlag = true;
802                        if (DEBUG) Log.w(TAG, String.format(
803                                "rport is changed: %d <> %d", mRPort, rPort));
804                        mRPort = rPort;
805                    } else {
806                        if (DEBUG_PING) Log.w(TAG, "rport is the same: " + rPort);
807                    }
808                } else {
809                    if (DEBUG) Log.w(TAG, "peer did not respond rport");
810                }
811                reset();
812                return true;
813            }
814            return false;
815        }
816
817        private int getRPortFromResponse(Response response) {
818            ViaHeader viaHeader = (ViaHeader)(response.getHeader(
819                    SIPHeaderNames.VIA));
820            return (viaHeader == null) ? -1 : viaHeader.getRPort();
821        }
822
823        private boolean registeringToReady(EventObject evt)
824                throws SipException {
825            if (expectResponse(Request.REGISTER, evt)) {
826                ResponseEvent event = (ResponseEvent) evt;
827                Response response = event.getResponse();
828
829                int statusCode = response.getStatusCode();
830                switch (statusCode) {
831                case Response.OK:
832                    int state = mState;
833                    onRegistrationDone((state == SipSession.State.REGISTERING)
834                            ? getExpiryTime(((ResponseEvent) evt).getResponse())
835                            : -1);
836                    return true;
837                case Response.UNAUTHORIZED:
838                case Response.PROXY_AUTHENTICATION_REQUIRED:
839                    handleAuthentication(event);
840                    return true;
841                default:
842                    if (statusCode >= 500) {
843                        onRegistrationFailed(response);
844                        return true;
845                    }
846                }
847            }
848            return false;
849        }
850
851        private boolean handleAuthentication(ResponseEvent event)
852                throws SipException {
853            Response response = event.getResponse();
854            String nonce = getNonceFromResponse(response);
855            if (nonce == null) {
856                onError(SipErrorCode.SERVER_ERROR,
857                        "server does not provide challenge");
858                return false;
859            } else if (mAuthenticationRetryCount < 2) {
860                mClientTransaction = mSipHelper.handleChallenge(
861                        event, getAccountManager());
862                mDialog = mClientTransaction.getDialog();
863                mAuthenticationRetryCount++;
864                if (isLoggable(this, event)) {
865                    Log.d(TAG, "   authentication retry count="
866                            + mAuthenticationRetryCount);
867                }
868                return true;
869            } else {
870                if (crossDomainAuthenticationRequired(response)) {
871                    onError(SipErrorCode.CROSS_DOMAIN_AUTHENTICATION,
872                            getRealmFromResponse(response));
873                } else {
874                    onError(SipErrorCode.INVALID_CREDENTIALS,
875                            "incorrect username or password");
876                }
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                            String username = mLocalProfile.getAuthUserName();
894                            return (!TextUtils.isEmpty(username) ? username :
895                                    mLocalProfile.getUserName());
896                        }
897
898                        public String getPassword() {
899                            return mPassword;
900                        }
901
902                        public String getSipDomain() {
903                            return mLocalProfile.getSipDomain();
904                        }
905                    };
906                }
907            };
908        }
909
910        private String getRealmFromResponse(Response response) {
911            WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
912                    SIPHeaderNames.WWW_AUTHENTICATE);
913            if (wwwAuth != null) return wwwAuth.getRealm();
914            ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
915                    SIPHeaderNames.PROXY_AUTHENTICATE);
916            return (proxyAuth == null) ? null : proxyAuth.getRealm();
917        }
918
919        private String getNonceFromResponse(Response response) {
920            WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
921                    SIPHeaderNames.WWW_AUTHENTICATE);
922            if (wwwAuth != null) return wwwAuth.getNonce();
923            ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
924                    SIPHeaderNames.PROXY_AUTHENTICATE);
925            return (proxyAuth == null) ? null : proxyAuth.getNonce();
926        }
927
928        private boolean readyForCall(EventObject evt) throws SipException {
929            // expect MakeCallCommand, RegisterCommand, DEREGISTER
930            if (evt instanceof MakeCallCommand) {
931                mState = SipSession.State.OUTGOING_CALL;
932                MakeCallCommand cmd = (MakeCallCommand) evt;
933                mPeerProfile = cmd.getPeerProfile();
934                mClientTransaction = mSipHelper.sendInvite(mLocalProfile,
935                        mPeerProfile, cmd.getSessionDescription(),
936                        generateTag());
937                mDialog = mClientTransaction.getDialog();
938                addSipSession(this);
939                startSessionTimer(cmd.getTimeout());
940                mProxy.onCalling(this);
941                return true;
942            } else if (evt instanceof RegisterCommand) {
943                mState = SipSession.State.REGISTERING;
944                int duration = ((RegisterCommand) evt).getDuration();
945                mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
946                        generateTag(), duration);
947                mDialog = mClientTransaction.getDialog();
948                addSipSession(this);
949                mProxy.onRegistering(this);
950                return true;
951            } else if (DEREGISTER == evt) {
952                mState = SipSession.State.DEREGISTERING;
953                mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
954                        generateTag(), 0);
955                mDialog = mClientTransaction.getDialog();
956                addSipSession(this);
957                mProxy.onRegistering(this);
958                return true;
959            }
960            return false;
961        }
962
963        private boolean incomingCall(EventObject evt) throws SipException {
964            // expect MakeCallCommand(answering) , END_CALL cmd , Cancel
965            if (evt instanceof MakeCallCommand) {
966                // answer call
967                mState = SipSession.State.INCOMING_CALL_ANSWERING;
968                mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived,
969                        mLocalProfile,
970                        ((MakeCallCommand) evt).getSessionDescription(),
971                        mServerTransaction);
972                startSessionTimer(((MakeCallCommand) evt).getTimeout());
973                return true;
974            } else if (END_CALL == evt) {
975                mSipHelper.sendInviteBusyHere(mInviteReceived,
976                        mServerTransaction);
977                endCallNormally();
978                return true;
979            } else if (isRequestEvent(Request.CANCEL, evt)) {
980                RequestEvent event = (RequestEvent) evt;
981                mSipHelper.sendResponse(event, Response.OK);
982                mSipHelper.sendInviteRequestTerminated(
983                        mInviteReceived.getRequest(), mServerTransaction);
984                endCallNormally();
985                return true;
986            }
987            return false;
988        }
989
990        private boolean incomingCallToInCall(EventObject evt)
991                throws SipException {
992            // expect ACK, CANCEL request
993            if (isRequestEvent(Request.ACK, evt)) {
994                establishCall();
995                return true;
996            } else if (isRequestEvent(Request.CANCEL, evt)) {
997                // http://tools.ietf.org/html/rfc3261#section-9.2
998                // Final response has been sent; do nothing here.
999                return true;
1000            }
1001            return false;
1002        }
1003
1004        private boolean outgoingCall(EventObject evt) throws SipException {
1005            if (expectResponse(Request.INVITE, evt)) {
1006                ResponseEvent event = (ResponseEvent) evt;
1007                Response response = event.getResponse();
1008
1009                int statusCode = response.getStatusCode();
1010                switch (statusCode) {
1011                case Response.RINGING:
1012                case Response.CALL_IS_BEING_FORWARDED:
1013                case Response.QUEUED:
1014                case Response.SESSION_PROGRESS:
1015                    // feedback any provisional responses (except TRYING) as
1016                    // ring back for better UX
1017                    if (mState == SipSession.State.OUTGOING_CALL) {
1018                        mState = SipSession.State.OUTGOING_CALL_RING_BACK;
1019                        cancelSessionTimer();
1020                        mProxy.onRingingBack(this);
1021                    }
1022                    return true;
1023                case Response.OK:
1024                    mSipHelper.sendInviteAck(event, mDialog);
1025                    mPeerSessionDescription = extractContent(response);
1026                    establishCall();
1027                    return true;
1028                case Response.UNAUTHORIZED:
1029                case Response.PROXY_AUTHENTICATION_REQUIRED:
1030                    if (handleAuthentication(event)) {
1031                        addSipSession(this);
1032                    }
1033                    return true;
1034                case Response.REQUEST_PENDING:
1035                    // TODO:
1036                    // rfc3261#section-14.1; re-schedule invite
1037                    return true;
1038                default:
1039                    if (statusCode >= 400) {
1040                        // error: an ack is sent automatically by the stack
1041                        onError(response);
1042                        return true;
1043                    } else if (statusCode >= 300) {
1044                        // TODO: handle 3xx (redirect)
1045                    } else {
1046                        return true;
1047                    }
1048                }
1049                return false;
1050            } else if (END_CALL == evt) {
1051                // RFC says that UA should not send out cancel when no
1052                // response comes back yet. We are cheating for not checking
1053                // response.
1054                mState = SipSession.State.OUTGOING_CALL_CANCELING;
1055                mSipHelper.sendCancel(mClientTransaction);
1056                startSessionTimer(CANCEL_CALL_TIMER);
1057                return true;
1058            } else if (isRequestEvent(Request.INVITE, evt)) {
1059                // Call self? Send BUSY HERE so server may redirect the call to
1060                // voice mailbox.
1061                RequestEvent event = (RequestEvent) evt;
1062                mSipHelper.sendInviteBusyHere(event,
1063                        event.getServerTransaction());
1064                return true;
1065            }
1066            return false;
1067        }
1068
1069        private boolean outgoingCallToReady(EventObject evt)
1070                throws SipException {
1071            if (evt instanceof ResponseEvent) {
1072                ResponseEvent event = (ResponseEvent) evt;
1073                Response response = event.getResponse();
1074                int statusCode = response.getStatusCode();
1075                if (expectResponse(Request.CANCEL, evt)) {
1076                    if (statusCode == Response.OK) {
1077                        // do nothing; wait for REQUEST_TERMINATED
1078                        return true;
1079                    }
1080                } else if (expectResponse(Request.INVITE, evt)) {
1081                    switch (statusCode) {
1082                        case Response.OK:
1083                            outgoingCall(evt); // abort Cancel
1084                            return true;
1085                        case Response.REQUEST_TERMINATED:
1086                            endCallNormally();
1087                            return true;
1088                    }
1089                } else {
1090                    return false;
1091                }
1092
1093                if (statusCode >= 400) {
1094                    onError(response);
1095                    return true;
1096                }
1097            } else if (evt instanceof TransactionTerminatedEvent) {
1098                // rfc3261#section-14.1:
1099                // if re-invite gets timed out, terminate the dialog; but
1100                // re-invite is not reliable, just let it go and pretend
1101                // nothing happened.
1102                onError(new SipException("timed out"));
1103            }
1104            return false;
1105        }
1106
1107        private boolean inCall(EventObject evt) throws SipException {
1108            // expect END_CALL cmd, BYE request, hold call (MakeCallCommand)
1109            // OK retransmission is handled in SipStack
1110            if (END_CALL == evt) {
1111                // rfc3261#section-15.1.1
1112                mSipHelper.sendBye(mDialog);
1113                endCallNormally();
1114                return true;
1115            } else if (isRequestEvent(Request.INVITE, evt)) {
1116                // got Re-INVITE
1117                mState = SipSession.State.INCOMING_CALL;
1118                RequestEvent event = mInviteReceived = (RequestEvent) evt;
1119                mPeerSessionDescription = extractContent(event.getRequest());
1120                mServerTransaction = null;
1121                mProxy.onRinging(this, mPeerProfile, mPeerSessionDescription);
1122                return true;
1123            } else if (isRequestEvent(Request.BYE, evt)) {
1124                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
1125                endCallNormally();
1126                return true;
1127            } else if (evt instanceof MakeCallCommand) {
1128                // to change call
1129                mState = SipSession.State.OUTGOING_CALL;
1130                mClientTransaction = mSipHelper.sendReinvite(mDialog,
1131                        ((MakeCallCommand) evt).getSessionDescription());
1132                startSessionTimer(((MakeCallCommand) evt).getTimeout());
1133                return true;
1134            }
1135            return false;
1136        }
1137
1138        // timeout in seconds
1139        private void startSessionTimer(int timeout) {
1140            if (timeout > 0) {
1141                mTimer = new SessionTimer();
1142                mTimer.start(timeout);
1143            }
1144        }
1145
1146        private void cancelSessionTimer() {
1147            if (mTimer != null) {
1148                mTimer.cancel();
1149                mTimer = null;
1150            }
1151        }
1152
1153        private String createErrorMessage(Response response) {
1154            return String.format("%s (%d)", response.getReasonPhrase(),
1155                    response.getStatusCode());
1156        }
1157
1158        private void establishCall() {
1159            mState = SipSession.State.IN_CALL;
1160            mInCall = true;
1161            cancelSessionTimer();
1162            mProxy.onCallEstablished(this, mPeerSessionDescription);
1163        }
1164
1165        private void endCallNormally() {
1166            reset();
1167            mProxy.onCallEnded(this);
1168        }
1169
1170        private void endCallOnError(int errorCode, String message) {
1171            reset();
1172            mProxy.onError(this, errorCode, message);
1173        }
1174
1175        private void endCallOnBusy() {
1176            reset();
1177            mProxy.onCallBusy(this);
1178        }
1179
1180        private void onError(int errorCode, String message) {
1181            cancelSessionTimer();
1182            switch (mState) {
1183                case SipSession.State.REGISTERING:
1184                case SipSession.State.DEREGISTERING:
1185                    onRegistrationFailed(errorCode, message);
1186                    break;
1187                default:
1188                    endCallOnError(errorCode, message);
1189            }
1190        }
1191
1192
1193        private void onError(Throwable exception) {
1194            exception = getRootCause(exception);
1195            onError(getErrorCode(exception), exception.toString());
1196        }
1197
1198        private void onError(Response response) {
1199            int statusCode = response.getStatusCode();
1200            if (!mInCall && (statusCode == Response.BUSY_HERE)) {
1201                endCallOnBusy();
1202            } else {
1203                onError(getErrorCode(statusCode), createErrorMessage(response));
1204            }
1205        }
1206
1207        private int getErrorCode(int responseStatusCode) {
1208            switch (responseStatusCode) {
1209                case Response.TEMPORARILY_UNAVAILABLE:
1210                case Response.FORBIDDEN:
1211                case Response.GONE:
1212                case Response.NOT_FOUND:
1213                case Response.NOT_ACCEPTABLE:
1214                case Response.NOT_ACCEPTABLE_HERE:
1215                    return SipErrorCode.PEER_NOT_REACHABLE;
1216
1217                case Response.REQUEST_URI_TOO_LONG:
1218                case Response.ADDRESS_INCOMPLETE:
1219                case Response.AMBIGUOUS:
1220                    return SipErrorCode.INVALID_REMOTE_URI;
1221
1222                case Response.REQUEST_TIMEOUT:
1223                    return SipErrorCode.TIME_OUT;
1224
1225                default:
1226                    if (responseStatusCode < 500) {
1227                        return SipErrorCode.CLIENT_ERROR;
1228                    } else {
1229                        return SipErrorCode.SERVER_ERROR;
1230                    }
1231            }
1232        }
1233
1234        private Throwable getRootCause(Throwable exception) {
1235            Throwable cause = exception.getCause();
1236            while (cause != null) {
1237                exception = cause;
1238                cause = exception.getCause();
1239            }
1240            return exception;
1241        }
1242
1243        private int getErrorCode(Throwable exception) {
1244            String message = exception.getMessage();
1245            if (exception instanceof UnknownHostException) {
1246                return SipErrorCode.SERVER_UNREACHABLE;
1247            } else if (exception instanceof IOException) {
1248                return SipErrorCode.SOCKET_ERROR;
1249            } else {
1250                return SipErrorCode.CLIENT_ERROR;
1251            }
1252        }
1253
1254        private void onRegistrationDone(int duration) {
1255            reset();
1256            mProxy.onRegistrationDone(this, duration);
1257        }
1258
1259        private void onRegistrationFailed(int errorCode, String message) {
1260            reset();
1261            mProxy.onRegistrationFailed(this, errorCode, message);
1262        }
1263
1264        private void onRegistrationFailed(Throwable exception) {
1265            exception = getRootCause(exception);
1266            onRegistrationFailed(getErrorCode(exception),
1267                    exception.toString());
1268        }
1269
1270        private void onRegistrationFailed(Response response) {
1271            int statusCode = response.getStatusCode();
1272            onRegistrationFailed(getErrorCode(statusCode),
1273                    createErrorMessage(response));
1274        }
1275    }
1276
1277    /**
1278     * @return true if the event is a request event matching the specified
1279     *      method; false otherwise
1280     */
1281    private static boolean isRequestEvent(String method, EventObject event) {
1282        try {
1283            if (event instanceof RequestEvent) {
1284                RequestEvent requestEvent = (RequestEvent) event;
1285                return method.equals(requestEvent.getRequest().getMethod());
1286            }
1287        } catch (Throwable e) {
1288        }
1289        return false;
1290    }
1291
1292    private static String getCseqMethod(Message message) {
1293        return ((CSeqHeader) message.getHeader(CSeqHeader.NAME)).getMethod();
1294    }
1295
1296    /**
1297     * @return true if the event is a response event and the CSeqHeader method
1298     * match the given arguments; false otherwise
1299     */
1300    private static boolean expectResponse(
1301            String expectedMethod, EventObject evt) {
1302        if (evt instanceof ResponseEvent) {
1303            ResponseEvent event = (ResponseEvent) evt;
1304            Response response = event.getResponse();
1305            return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
1306        }
1307        return false;
1308    }
1309
1310    /**
1311     * @return true if the event is a response event and the response code and
1312     *      CSeqHeader method match the given arguments; false otherwise
1313     */
1314    private static boolean expectResponse(
1315            int responseCode, String expectedMethod, EventObject evt) {
1316        if (evt instanceof ResponseEvent) {
1317            ResponseEvent event = (ResponseEvent) evt;
1318            Response response = event.getResponse();
1319            if (response.getStatusCode() == responseCode) {
1320                return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
1321            }
1322        }
1323        return false;
1324    }
1325
1326    private static SipProfile createPeerProfile(Request request)
1327            throws SipException {
1328        try {
1329            FromHeader fromHeader =
1330                    (FromHeader) request.getHeader(FromHeader.NAME);
1331            Address address = fromHeader.getAddress();
1332            SipURI uri = (SipURI) address.getURI();
1333            String username = uri.getUser();
1334            if (username == null) username = ANONYMOUS;
1335            int port = uri.getPort();
1336            SipProfile.Builder builder =
1337                    new SipProfile.Builder(username, uri.getHost())
1338                    .setDisplayName(address.getDisplayName());
1339            if (port > 0) builder.setPort(port);
1340            return builder.build();
1341        } catch (IllegalArgumentException e) {
1342            throw new SipException("createPeerProfile()", e);
1343        } catch (ParseException e) {
1344            throw new SipException("createPeerProfile()", e);
1345        }
1346    }
1347
1348    private static boolean isLoggable(SipSessionImpl s) {
1349        if (s != null) {
1350            switch (s.mState) {
1351                case SipSession.State.PINGING:
1352                    return DEBUG_PING;
1353            }
1354        }
1355        return DEBUG;
1356    }
1357
1358    private static boolean isLoggable(EventObject evt) {
1359        return isLoggable(null, evt);
1360    }
1361
1362    private static boolean isLoggable(SipSessionImpl s, EventObject evt) {
1363        if (!isLoggable(s)) return false;
1364        if (evt == null) return false;
1365
1366        if (evt instanceof OptionsCommand) {
1367            return DEBUG_PING;
1368        } else if (evt instanceof ResponseEvent) {
1369            Response response = ((ResponseEvent) evt).getResponse();
1370            if (Request.OPTIONS.equals(response.getHeader(CSeqHeader.NAME))) {
1371                return DEBUG_PING;
1372            }
1373            return DEBUG;
1374        } else if (evt instanceof RequestEvent) {
1375            return DEBUG;
1376        }
1377        return false;
1378    }
1379
1380    private static String log(EventObject evt) {
1381        if (evt instanceof RequestEvent) {
1382            return ((RequestEvent) evt).getRequest().toString();
1383        } else if (evt instanceof ResponseEvent) {
1384            return ((ResponseEvent) evt).getResponse().toString();
1385        } else {
1386            return evt.toString();
1387        }
1388    }
1389
1390    private class OptionsCommand extends EventObject {
1391        public OptionsCommand() {
1392            super(SipSessionGroup.this);
1393        }
1394    }
1395
1396    private class RegisterCommand extends EventObject {
1397        private int mDuration;
1398
1399        public RegisterCommand(int duration) {
1400            super(SipSessionGroup.this);
1401            mDuration = duration;
1402        }
1403
1404        public int getDuration() {
1405            return mDuration;
1406        }
1407    }
1408
1409    private class MakeCallCommand extends EventObject {
1410        private String mSessionDescription;
1411        private int mTimeout; // in seconds
1412
1413        public MakeCallCommand(SipProfile peerProfile,
1414                String sessionDescription) {
1415            this(peerProfile, sessionDescription, -1);
1416        }
1417
1418        public MakeCallCommand(SipProfile peerProfile,
1419                String sessionDescription, int timeout) {
1420            super(peerProfile);
1421            mSessionDescription = sessionDescription;
1422            mTimeout = timeout;
1423        }
1424
1425        public SipProfile getPeerProfile() {
1426            return (SipProfile) getSource();
1427        }
1428
1429        public String getSessionDescription() {
1430            return mSessionDescription;
1431        }
1432
1433        public int getTimeout() {
1434            return mTimeout;
1435        }
1436    }
1437}
1438