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            synchronized (SipSessionGroup.this) {
531                if (mPeerProfile == null) return;
532                try {
533                    processCommand(new MakeCallCommand(mPeerProfile,
534                            sessionDescription, timeout));
535                } catch (SipException e) {
536                    onError(e);
537                }
538            }
539        }
540
541        public void endCall() {
542            doCommandAsync(END_CALL);
543        }
544
545        public void changeCall(String sessionDescription, int timeout) {
546            synchronized (SipSessionGroup.this) {
547                if (mPeerProfile == null) return;
548                doCommandAsync(new MakeCallCommand(mPeerProfile,
549                        sessionDescription, timeout));
550            }
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                if (crossDomainAuthenticationRequired(response)) {
876                    onError(SipErrorCode.CROSS_DOMAIN_AUTHENTICATION,
877                            getRealmFromResponse(response));
878                } else {
879                    onError(SipErrorCode.INVALID_CREDENTIALS,
880                            "incorrect username or password");
881                }
882                return false;
883            }
884        }
885
886        private boolean crossDomainAuthenticationRequired(Response response) {
887            String realm = getRealmFromResponse(response);
888            if (realm == null) realm = "";
889            return !mLocalProfile.getSipDomain().trim().equals(realm.trim());
890        }
891
892        private AccountManager getAccountManager() {
893            return new AccountManager() {
894                public UserCredentials getCredentials(ClientTransaction
895                        challengedTransaction, String realm) {
896                    return new UserCredentials() {
897                        public String getUserName() {
898                            String username = mLocalProfile.getAuthUserName();
899                            return (!TextUtils.isEmpty(username) ? username :
900                                    mLocalProfile.getUserName());
901                        }
902
903                        public String getPassword() {
904                            return mPassword;
905                        }
906
907                        public String getSipDomain() {
908                            return mLocalProfile.getSipDomain();
909                        }
910                    };
911                }
912            };
913        }
914
915        private String getRealmFromResponse(Response response) {
916            WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
917                    SIPHeaderNames.WWW_AUTHENTICATE);
918            if (wwwAuth != null) return wwwAuth.getRealm();
919            ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
920                    SIPHeaderNames.PROXY_AUTHENTICATE);
921            return (proxyAuth == null) ? null : proxyAuth.getRealm();
922        }
923
924        private String getNonceFromResponse(Response response) {
925            WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
926                    SIPHeaderNames.WWW_AUTHENTICATE);
927            if (wwwAuth != null) return wwwAuth.getNonce();
928            ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
929                    SIPHeaderNames.PROXY_AUTHENTICATE);
930            return (proxyAuth == null) ? null : proxyAuth.getNonce();
931        }
932
933        private boolean readyForCall(EventObject evt) throws SipException {
934            // expect MakeCallCommand, RegisterCommand, DEREGISTER
935            if (evt instanceof MakeCallCommand) {
936                mState = SipSession.State.OUTGOING_CALL;
937                MakeCallCommand cmd = (MakeCallCommand) evt;
938                mPeerProfile = cmd.getPeerProfile();
939                mClientTransaction = mSipHelper.sendInvite(mLocalProfile,
940                        mPeerProfile, cmd.getSessionDescription(),
941                        generateTag());
942                mDialog = mClientTransaction.getDialog();
943                addSipSession(this);
944                startSessionTimer(cmd.getTimeout());
945                mProxy.onCalling(this);
946                return true;
947            } else if (evt instanceof RegisterCommand) {
948                mState = SipSession.State.REGISTERING;
949                int duration = ((RegisterCommand) evt).getDuration();
950                mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
951                        generateTag(), duration);
952                mDialog = mClientTransaction.getDialog();
953                addSipSession(this);
954                mProxy.onRegistering(this);
955                return true;
956            } else if (DEREGISTER == evt) {
957                mState = SipSession.State.DEREGISTERING;
958                mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
959                        generateTag(), 0);
960                mDialog = mClientTransaction.getDialog();
961                addSipSession(this);
962                mProxy.onRegistering(this);
963                return true;
964            }
965            return false;
966        }
967
968        private boolean incomingCall(EventObject evt) throws SipException {
969            // expect MakeCallCommand(answering) , END_CALL cmd , Cancel
970            if (evt instanceof MakeCallCommand) {
971                // answer call
972                mState = SipSession.State.INCOMING_CALL_ANSWERING;
973                mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived,
974                        mLocalProfile,
975                        ((MakeCallCommand) evt).getSessionDescription(),
976                        mServerTransaction);
977                startSessionTimer(((MakeCallCommand) evt).getTimeout());
978                return true;
979            } else if (END_CALL == evt) {
980                mSipHelper.sendInviteBusyHere(mInviteReceived,
981                        mServerTransaction);
982                endCallNormally();
983                return true;
984            } else if (isRequestEvent(Request.CANCEL, evt)) {
985                RequestEvent event = (RequestEvent) evt;
986                mSipHelper.sendResponse(event, Response.OK);
987                mSipHelper.sendInviteRequestTerminated(
988                        mInviteReceived.getRequest(), mServerTransaction);
989                endCallNormally();
990                return true;
991            }
992            return false;
993        }
994
995        private boolean incomingCallToInCall(EventObject evt)
996                throws SipException {
997            // expect ACK, CANCEL request
998            if (isRequestEvent(Request.ACK, evt)) {
999                establishCall();
1000                return true;
1001            } else if (isRequestEvent(Request.CANCEL, evt)) {
1002                // http://tools.ietf.org/html/rfc3261#section-9.2
1003                // Final response has been sent; do nothing here.
1004                return true;
1005            }
1006            return false;
1007        }
1008
1009        private boolean outgoingCall(EventObject evt) throws SipException {
1010            if (expectResponse(Request.INVITE, evt)) {
1011                ResponseEvent event = (ResponseEvent) evt;
1012                Response response = event.getResponse();
1013
1014                int statusCode = response.getStatusCode();
1015                switch (statusCode) {
1016                case Response.RINGING:
1017                case Response.CALL_IS_BEING_FORWARDED:
1018                case Response.QUEUED:
1019                case Response.SESSION_PROGRESS:
1020                    // feedback any provisional responses (except TRYING) as
1021                    // ring back for better UX
1022                    if (mState == SipSession.State.OUTGOING_CALL) {
1023                        mState = SipSession.State.OUTGOING_CALL_RING_BACK;
1024                        cancelSessionTimer();
1025                        mProxy.onRingingBack(this);
1026                    }
1027                    return true;
1028                case Response.OK:
1029                    mSipHelper.sendInviteAck(event, mDialog);
1030                    mPeerSessionDescription = extractContent(response);
1031                    establishCall();
1032                    return true;
1033                case Response.UNAUTHORIZED:
1034                case Response.PROXY_AUTHENTICATION_REQUIRED:
1035                    if (handleAuthentication(event)) {
1036                        addSipSession(this);
1037                    }
1038                    return true;
1039                case Response.REQUEST_PENDING:
1040                    // TODO:
1041                    // rfc3261#section-14.1; re-schedule invite
1042                    return true;
1043                default:
1044                    if (statusCode >= 400) {
1045                        // error: an ack is sent automatically by the stack
1046                        onError(response);
1047                        return true;
1048                    } else if (statusCode >= 300) {
1049                        // TODO: handle 3xx (redirect)
1050                    } else {
1051                        return true;
1052                    }
1053                }
1054                return false;
1055            } else if (END_CALL == evt) {
1056                // RFC says that UA should not send out cancel when no
1057                // response comes back yet. We are cheating for not checking
1058                // response.
1059                mState = SipSession.State.OUTGOING_CALL_CANCELING;
1060                mSipHelper.sendCancel(mClientTransaction);
1061                startSessionTimer(CANCEL_CALL_TIMER);
1062                return true;
1063            } else if (isRequestEvent(Request.INVITE, evt)) {
1064                // Call self? Send BUSY HERE so server may redirect the call to
1065                // voice mailbox.
1066                RequestEvent event = (RequestEvent) evt;
1067                mSipHelper.sendInviteBusyHere(event,
1068                        event.getServerTransaction());
1069                return true;
1070            }
1071            return false;
1072        }
1073
1074        private boolean outgoingCallToReady(EventObject evt)
1075                throws SipException {
1076            if (evt instanceof ResponseEvent) {
1077                ResponseEvent event = (ResponseEvent) evt;
1078                Response response = event.getResponse();
1079                int statusCode = response.getStatusCode();
1080                if (expectResponse(Request.CANCEL, evt)) {
1081                    if (statusCode == Response.OK) {
1082                        // do nothing; wait for REQUEST_TERMINATED
1083                        return true;
1084                    }
1085                } else if (expectResponse(Request.INVITE, evt)) {
1086                    switch (statusCode) {
1087                        case Response.OK:
1088                            outgoingCall(evt); // abort Cancel
1089                            return true;
1090                        case Response.REQUEST_TERMINATED:
1091                            endCallNormally();
1092                            return true;
1093                    }
1094                } else {
1095                    return false;
1096                }
1097
1098                if (statusCode >= 400) {
1099                    onError(response);
1100                    return true;
1101                }
1102            } else if (evt instanceof TransactionTerminatedEvent) {
1103                // rfc3261#section-14.1:
1104                // if re-invite gets timed out, terminate the dialog; but
1105                // re-invite is not reliable, just let it go and pretend
1106                // nothing happened.
1107                onError(new SipException("timed out"));
1108            }
1109            return false;
1110        }
1111
1112        private boolean inCall(EventObject evt) throws SipException {
1113            // expect END_CALL cmd, BYE request, hold call (MakeCallCommand)
1114            // OK retransmission is handled in SipStack
1115            if (END_CALL == evt) {
1116                // rfc3261#section-15.1.1
1117                mSipHelper.sendBye(mDialog);
1118                endCallNormally();
1119                return true;
1120            } else if (isRequestEvent(Request.INVITE, evt)) {
1121                // got Re-INVITE
1122                mState = SipSession.State.INCOMING_CALL;
1123                RequestEvent event = mInviteReceived = (RequestEvent) evt;
1124                mPeerSessionDescription = extractContent(event.getRequest());
1125                mServerTransaction = null;
1126                mProxy.onRinging(this, mPeerProfile, mPeerSessionDescription);
1127                return true;
1128            } else if (isRequestEvent(Request.BYE, evt)) {
1129                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
1130                endCallNormally();
1131                return true;
1132            } else if (evt instanceof MakeCallCommand) {
1133                // to change call
1134                mState = SipSession.State.OUTGOING_CALL;
1135                mClientTransaction = mSipHelper.sendReinvite(mDialog,
1136                        ((MakeCallCommand) evt).getSessionDescription());
1137                startSessionTimer(((MakeCallCommand) evt).getTimeout());
1138                return true;
1139            }
1140            return false;
1141        }
1142
1143        // timeout in seconds
1144        private void startSessionTimer(int timeout) {
1145            if (timeout > 0) {
1146                mTimer = new SessionTimer();
1147                mTimer.start(timeout);
1148            }
1149        }
1150
1151        private void cancelSessionTimer() {
1152            if (mTimer != null) {
1153                mTimer.cancel();
1154                mTimer = null;
1155            }
1156        }
1157
1158        private String createErrorMessage(Response response) {
1159            return String.format("%s (%d)", response.getReasonPhrase(),
1160                    response.getStatusCode());
1161        }
1162
1163        private void establishCall() {
1164            mState = SipSession.State.IN_CALL;
1165            mInCall = true;
1166            cancelSessionTimer();
1167            mProxy.onCallEstablished(this, mPeerSessionDescription);
1168        }
1169
1170        private void endCallNormally() {
1171            reset();
1172            mProxy.onCallEnded(this);
1173        }
1174
1175        private void endCallOnError(int errorCode, String message) {
1176            reset();
1177            mProxy.onError(this, errorCode, message);
1178        }
1179
1180        private void endCallOnBusy() {
1181            reset();
1182            mProxy.onCallBusy(this);
1183        }
1184
1185        private void onError(int errorCode, String message) {
1186            cancelSessionTimer();
1187            switch (mState) {
1188                case SipSession.State.REGISTERING:
1189                case SipSession.State.DEREGISTERING:
1190                    onRegistrationFailed(errorCode, message);
1191                    break;
1192                default:
1193                    endCallOnError(errorCode, message);
1194            }
1195        }
1196
1197
1198        private void onError(Throwable exception) {
1199            exception = getRootCause(exception);
1200            onError(getErrorCode(exception), exception.toString());
1201        }
1202
1203        private void onError(Response response) {
1204            int statusCode = response.getStatusCode();
1205            if (!mInCall && (statusCode == Response.BUSY_HERE)) {
1206                endCallOnBusy();
1207            } else {
1208                onError(getErrorCode(statusCode), createErrorMessage(response));
1209            }
1210        }
1211
1212        private int getErrorCode(int responseStatusCode) {
1213            switch (responseStatusCode) {
1214                case Response.TEMPORARILY_UNAVAILABLE:
1215                case Response.FORBIDDEN:
1216                case Response.GONE:
1217                case Response.NOT_FOUND:
1218                case Response.NOT_ACCEPTABLE:
1219                case Response.NOT_ACCEPTABLE_HERE:
1220                    return SipErrorCode.PEER_NOT_REACHABLE;
1221
1222                case Response.REQUEST_URI_TOO_LONG:
1223                case Response.ADDRESS_INCOMPLETE:
1224                case Response.AMBIGUOUS:
1225                    return SipErrorCode.INVALID_REMOTE_URI;
1226
1227                case Response.REQUEST_TIMEOUT:
1228                    return SipErrorCode.TIME_OUT;
1229
1230                default:
1231                    if (responseStatusCode < 500) {
1232                        return SipErrorCode.CLIENT_ERROR;
1233                    } else {
1234                        return SipErrorCode.SERVER_ERROR;
1235                    }
1236            }
1237        }
1238
1239        private Throwable getRootCause(Throwable exception) {
1240            Throwable cause = exception.getCause();
1241            while (cause != null) {
1242                exception = cause;
1243                cause = exception.getCause();
1244            }
1245            return exception;
1246        }
1247
1248        private int getErrorCode(Throwable exception) {
1249            String message = exception.getMessage();
1250            if (exception instanceof UnknownHostException) {
1251                return SipErrorCode.SERVER_UNREACHABLE;
1252            } else if (exception instanceof IOException) {
1253                return SipErrorCode.SOCKET_ERROR;
1254            } else {
1255                return SipErrorCode.CLIENT_ERROR;
1256            }
1257        }
1258
1259        private void onRegistrationDone(int duration) {
1260            reset();
1261            mProxy.onRegistrationDone(this, duration);
1262        }
1263
1264        private void onRegistrationFailed(int errorCode, String message) {
1265            reset();
1266            mProxy.onRegistrationFailed(this, errorCode, message);
1267        }
1268
1269        private void onRegistrationFailed(Throwable exception) {
1270            exception = getRootCause(exception);
1271            onRegistrationFailed(getErrorCode(exception),
1272                    exception.toString());
1273        }
1274
1275        private void onRegistrationFailed(Response response) {
1276            int statusCode = response.getStatusCode();
1277            onRegistrationFailed(getErrorCode(statusCode),
1278                    createErrorMessage(response));
1279        }
1280    }
1281
1282    /**
1283     * @return true if the event is a request event matching the specified
1284     *      method; false otherwise
1285     */
1286    private static boolean isRequestEvent(String method, EventObject event) {
1287        try {
1288            if (event instanceof RequestEvent) {
1289                RequestEvent requestEvent = (RequestEvent) event;
1290                return method.equals(requestEvent.getRequest().getMethod());
1291            }
1292        } catch (Throwable e) {
1293        }
1294        return false;
1295    }
1296
1297    private static String getCseqMethod(Message message) {
1298        return ((CSeqHeader) message.getHeader(CSeqHeader.NAME)).getMethod();
1299    }
1300
1301    /**
1302     * @return true if the event is a response event and the CSeqHeader method
1303     * match the given arguments; false otherwise
1304     */
1305    private static boolean expectResponse(
1306            String expectedMethod, EventObject evt) {
1307        if (evt instanceof ResponseEvent) {
1308            ResponseEvent event = (ResponseEvent) evt;
1309            Response response = event.getResponse();
1310            return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
1311        }
1312        return false;
1313    }
1314
1315    /**
1316     * @return true if the event is a response event and the response code and
1317     *      CSeqHeader method match the given arguments; false otherwise
1318     */
1319    private static boolean expectResponse(
1320            int responseCode, String expectedMethod, EventObject evt) {
1321        if (evt instanceof ResponseEvent) {
1322            ResponseEvent event = (ResponseEvent) evt;
1323            Response response = event.getResponse();
1324            if (response.getStatusCode() == responseCode) {
1325                return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
1326            }
1327        }
1328        return false;
1329    }
1330
1331    private static SipProfile createPeerProfile(Request request)
1332            throws SipException {
1333        try {
1334            FromHeader fromHeader =
1335                    (FromHeader) request.getHeader(FromHeader.NAME);
1336            Address address = fromHeader.getAddress();
1337            SipURI uri = (SipURI) address.getURI();
1338            String username = uri.getUser();
1339            if (username == null) username = ANONYMOUS;
1340            int port = uri.getPort();
1341            SipProfile.Builder builder =
1342                    new SipProfile.Builder(username, uri.getHost())
1343                    .setDisplayName(address.getDisplayName());
1344            if (port > 0) builder.setPort(port);
1345            return builder.build();
1346        } catch (IllegalArgumentException e) {
1347            throw new SipException("createPeerProfile()", e);
1348        } catch (ParseException e) {
1349            throw new SipException("createPeerProfile()", e);
1350        }
1351    }
1352
1353    private static boolean isLoggable(SipSessionImpl s) {
1354        if (s != null) {
1355            switch (s.mState) {
1356                case SipSession.State.PINGING:
1357                    return DEBUG_PING;
1358            }
1359        }
1360        return DEBUG;
1361    }
1362
1363    private static boolean isLoggable(EventObject evt) {
1364        return isLoggable(null, evt);
1365    }
1366
1367    private static boolean isLoggable(SipSessionImpl s, EventObject evt) {
1368        if (!isLoggable(s)) return false;
1369        if (evt == null) return false;
1370
1371        if (evt instanceof OptionsCommand) {
1372            return DEBUG_PING;
1373        } else if (evt instanceof ResponseEvent) {
1374            Response response = ((ResponseEvent) evt).getResponse();
1375            if (Request.OPTIONS.equals(response.getHeader(CSeqHeader.NAME))) {
1376                return DEBUG_PING;
1377            }
1378            return DEBUG;
1379        } else if (evt instanceof RequestEvent) {
1380            return DEBUG;
1381        }
1382        return false;
1383    }
1384
1385    private static String log(EventObject evt) {
1386        if (evt instanceof RequestEvent) {
1387            return ((RequestEvent) evt).getRequest().toString();
1388        } else if (evt instanceof ResponseEvent) {
1389            return ((ResponseEvent) evt).getResponse().toString();
1390        } else {
1391            return evt.toString();
1392        }
1393    }
1394
1395    private class OptionsCommand extends EventObject {
1396        public OptionsCommand() {
1397            super(SipSessionGroup.this);
1398        }
1399    }
1400
1401    private class RegisterCommand extends EventObject {
1402        private int mDuration;
1403
1404        public RegisterCommand(int duration) {
1405            super(SipSessionGroup.this);
1406            mDuration = duration;
1407        }
1408
1409        public int getDuration() {
1410            return mDuration;
1411        }
1412    }
1413
1414    private class MakeCallCommand extends EventObject {
1415        private String mSessionDescription;
1416        private int mTimeout; // in seconds
1417
1418        public MakeCallCommand(SipProfile peerProfile,
1419                String sessionDescription) {
1420            this(peerProfile, sessionDescription, -1);
1421        }
1422
1423        public MakeCallCommand(SipProfile peerProfile,
1424                String sessionDescription, int timeout) {
1425            super(peerProfile);
1426            mSessionDescription = sessionDescription;
1427            mTimeout = timeout;
1428        }
1429
1430        public SipProfile getPeerProfile() {
1431            return (SipProfile) getSource();
1432        }
1433
1434        public String getSessionDescription() {
1435            return mSessionDescription;
1436        }
1437
1438        public int getTimeout() {
1439            return mTimeout;
1440        }
1441    }
1442}
1443