SipSessionGroup.java revision 0a6e717fb6846f66b8dc853e079f2166307bfc60
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        mSessionMap.clear();
194        closeToNotReceiveCalls();
195        if (mSipStack != null) {
196            mSipStack.stop();
197            mSipStack = null;
198            mSipHelper = null;
199        }
200    }
201
202    public synchronized boolean isClosed() {
203        return (mSipStack == null);
204    }
205
206    // For internal use, require listener not to block in callbacks.
207    public synchronized void openToReceiveCalls(ISipSessionListener listener) {
208        if (mCallReceiverSession == null) {
209            mCallReceiverSession = new SipSessionCallReceiverImpl(listener);
210        } else {
211            mCallReceiverSession.setListener(listener);
212        }
213    }
214
215    public synchronized void closeToNotReceiveCalls() {
216        mCallReceiverSession = null;
217    }
218
219    public ISipSession createSession(ISipSessionListener listener) {
220        return (isClosed() ? null : new SipSessionImpl(listener));
221    }
222
223    private static int allocateLocalPort() throws SipException {
224        try {
225            DatagramSocket s = new DatagramSocket();
226            int localPort = s.getLocalPort();
227            s.close();
228            return localPort;
229        } catch (IOException e) {
230            throw new SipException("allocateLocalPort()", e);
231        }
232    }
233
234    synchronized boolean containsSession(String callId) {
235        return mSessionMap.containsKey(callId);
236    }
237
238    private synchronized SipSessionImpl getSipSession(EventObject event) {
239        String key = SipHelper.getCallId(event);
240        SipSessionImpl session = mSessionMap.get(key);
241        if ((session != null) && isLoggable(session)) {
242            Log.d(TAG, "session key from event: " + key);
243            Log.d(TAG, "active sessions:");
244            for (String k : mSessionMap.keySet()) {
245                Log.d(TAG, " ..." + k + ": " + mSessionMap.get(k));
246            }
247        }
248        return ((session != null) ? session : mCallReceiverSession);
249    }
250
251    private synchronized void addSipSession(SipSessionImpl newSession) {
252        removeSipSession(newSession);
253        String key = newSession.getCallId();
254        mSessionMap.put(key, newSession);
255        if (isLoggable(newSession)) {
256            Log.d(TAG, "+++  add a session with key:  '" + key + "'");
257            for (String k : mSessionMap.keySet()) {
258                Log.d(TAG, "  " + k + ": " + mSessionMap.get(k));
259            }
260        }
261    }
262
263    private synchronized void removeSipSession(SipSessionImpl session) {
264        if (session == mCallReceiverSession) return;
265        String key = session.getCallId();
266        SipSessionImpl s = mSessionMap.remove(key);
267        // sanity check
268        if ((s != null) && (s != session)) {
269            Log.w(TAG, "session " + session + " is not associated with key '"
270                    + key + "'");
271            mSessionMap.put(key, s);
272            for (Map.Entry<String, SipSessionImpl> entry
273                    : mSessionMap.entrySet()) {
274                if (entry.getValue() == s) {
275                    key = entry.getKey();
276                    mSessionMap.remove(key);
277                }
278            }
279        }
280
281        if ((s != null) && isLoggable(s)) {
282            Log.d(TAG, "remove session " + session + " @key '" + key + "'");
283            for (String k : mSessionMap.keySet()) {
284                Log.d(TAG, "  " + k + ": " + mSessionMap.get(k));
285            }
286        }
287    }
288
289    public void processRequest(final RequestEvent event) {
290        if (isRequestEvent(Request.INVITE, event)) {
291            if (DEBUG) Log.d(TAG, "<<<<< got INVITE, thread:"
292                    + Thread.currentThread());
293            // Acquire a wake lock and keep it for WAKE_LOCK_HOLDING_TIME;
294            // should be large enough to bring up the app.
295            mWakeLock.acquire(WAKE_LOCK_HOLDING_TIME);
296        }
297        process(event);
298    }
299
300    public void processResponse(ResponseEvent event) {
301        process(event);
302    }
303
304    public void processIOException(IOExceptionEvent event) {
305        process(event);
306    }
307
308    public void processTimeout(TimeoutEvent event) {
309        process(event);
310    }
311
312    public void processTransactionTerminated(TransactionTerminatedEvent event) {
313        process(event);
314    }
315
316    public void processDialogTerminated(DialogTerminatedEvent event) {
317        process(event);
318    }
319
320    private synchronized void process(EventObject event) {
321        SipSessionImpl session = getSipSession(event);
322        try {
323            boolean isLoggable = isLoggable(session, event);
324            boolean processed = (session != null) && session.process(event);
325            if (isLoggable && processed) {
326                Log.d(TAG, "new state after: "
327                        + SipSession.State.toString(session.mState));
328            }
329        } catch (Throwable e) {
330            Log.w(TAG, "event process error: " + event, e);
331            session.onError(e);
332        }
333    }
334
335    private String extractContent(Message message) {
336        // Currently we do not support secure MIME bodies.
337        byte[] bytes = message.getRawContent();
338        if (bytes != null) {
339            try {
340                if (message instanceof SIPMessage) {
341                    return ((SIPMessage) message).getMessageContent();
342                } else {
343                    return new String(bytes, "UTF-8");
344                }
345            } catch (UnsupportedEncodingException e) {
346            }
347        }
348        return null;
349    }
350
351    private class SipSessionCallReceiverImpl extends SipSessionImpl {
352        public SipSessionCallReceiverImpl(ISipSessionListener listener) {
353            super(listener);
354        }
355
356        public boolean process(EventObject evt) throws SipException {
357            if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~   " + this + ": "
358                    + SipSession.State.toString(mState) + ": processing "
359                    + log(evt));
360            if (isRequestEvent(Request.INVITE, evt)) {
361                RequestEvent event = (RequestEvent) evt;
362                SipSessionImpl newSession = new SipSessionImpl(mProxy);
363                newSession.mState = SipSession.State.INCOMING_CALL;
364                newSession.mServerTransaction = mSipHelper.sendRinging(event,
365                        generateTag());
366                newSession.mDialog = newSession.mServerTransaction.getDialog();
367                newSession.mInviteReceived = event;
368                newSession.mPeerProfile = createPeerProfile(event.getRequest());
369                newSession.mPeerSessionDescription =
370                        extractContent(event.getRequest());
371                addSipSession(newSession);
372                mProxy.onRinging(newSession, newSession.mPeerProfile,
373                        newSession.mPeerSessionDescription);
374                return true;
375            } else if (isRequestEvent(Request.OPTIONS, evt)) {
376                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
377                return true;
378            } else {
379                return false;
380            }
381        }
382    }
383
384    class SipSessionImpl extends ISipSession.Stub {
385        SipProfile mPeerProfile;
386        SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
387        int mState = SipSession.State.READY_TO_CALL;
388        RequestEvent mInviteReceived;
389        Dialog mDialog;
390        ServerTransaction mServerTransaction;
391        ClientTransaction mClientTransaction;
392        String mPeerSessionDescription;
393        boolean mInCall;
394        SessionTimer mTimer;
395        int mAuthenticationRetryCount;
396
397        // for registration
398        boolean mReRegisterFlag = false;
399        int mRPort;
400
401        // lightweight timer
402        class SessionTimer {
403            private boolean mRunning = true;
404
405            void start(final int timeout) {
406                new Thread(new Runnable() {
407                    public void run() {
408                        sleep(timeout);
409                        if (mRunning) timeout();
410                    }
411                }, "SipSessionTimerThread").start();
412            }
413
414            synchronized void cancel() {
415                mRunning = false;
416                this.notify();
417            }
418
419            private void timeout() {
420                synchronized (SipSessionGroup.this) {
421                    onError(SipErrorCode.TIME_OUT, "Session timed out!");
422                }
423            }
424
425            private synchronized void sleep(int timeout) {
426                try {
427                    this.wait(timeout * 1000);
428                } catch (InterruptedException e) {
429                    Log.e(TAG, "session timer interrupted!");
430                }
431            }
432        }
433
434        public SipSessionImpl(ISipSessionListener listener) {
435            setListener(listener);
436        }
437
438        SipSessionImpl duplicate() {
439            return new SipSessionImpl(mProxy.getListener());
440        }
441
442        private void reset() {
443            mInCall = false;
444            removeSipSession(this);
445            mPeerProfile = null;
446            mState = SipSession.State.READY_TO_CALL;
447            mInviteReceived = null;
448            mPeerSessionDescription = null;
449            mRPort = 0;
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            try {
530                processCommand(new MakeCallCommand(mPeerProfile,
531                        sessionDescription, timeout));
532            } catch (SipException e) {
533                onError(e);
534            }
535        }
536
537        public void endCall() {
538            doCommandAsync(END_CALL);
539        }
540
541        public void changeCall(String sessionDescription, int timeout) {
542            doCommandAsync(new MakeCallCommand(mPeerProfile, sessionDescription,
543                    timeout));
544        }
545
546        public void changeCallWithTimeout(
547                String sessionDescription, int timeout) {
548            doCommandAsync(new MakeCallCommand(mPeerProfile, sessionDescription,
549                    timeout));
550        }
551
552        public void register(int duration) {
553            doCommandAsync(new RegisterCommand(duration));
554        }
555
556        public void unregister() {
557            doCommandAsync(DEREGISTER);
558        }
559
560        public boolean isReRegisterRequired() {
561            return mReRegisterFlag;
562        }
563
564        public void clearReRegisterRequired() {
565            mReRegisterFlag = false;
566        }
567
568        public void sendKeepAlive() {
569            mState = SipSession.State.PINGING;
570            try {
571                processCommand(new OptionsCommand());
572                for (int i = 0; i < 15; i++) {
573                    if (SipSession.State.PINGING != mState) break;
574                    Thread.sleep(200);
575                }
576                if (SipSession.State.PINGING == mState) {
577                    // FIXME: what to do if server doesn't respond
578                    reset();
579                    if (DEBUG) Log.w(TAG, "no response from ping");
580                }
581            } catch (SipException e) {
582                Log.e(TAG, "sendKeepAlive failed", e);
583            } catch (InterruptedException e) {
584                Log.e(TAG, "sendKeepAlive interrupted", e);
585            }
586        }
587
588        private void processCommand(EventObject command) throws SipException {
589            if (isLoggable(command)) Log.d(TAG, "process cmd: " + command);
590            if (!process(command)) {
591                onError(SipErrorCode.IN_PROGRESS,
592                        "cannot initiate a new transaction to execute: "
593                        + command);
594            }
595        }
596
597        protected String generateTag() {
598            // 32-bit randomness
599            return String.valueOf((long) (Math.random() * 0x100000000L));
600        }
601
602        public String toString() {
603            try {
604                String s = super.toString();
605                return s.substring(s.indexOf("@")) + ":"
606                        + SipSession.State.toString(mState);
607            } catch (Throwable e) {
608                return super.toString();
609            }
610        }
611
612        public boolean process(EventObject evt) throws SipException {
613            if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~   " + this + ": "
614                    + SipSession.State.toString(mState) + ": processing "
615                    + log(evt));
616            synchronized (SipSessionGroup.this) {
617                if (isClosed()) return false;
618
619                Dialog dialog = null;
620                if (evt instanceof RequestEvent) {
621                    dialog = ((RequestEvent) evt).getDialog();
622                } else if (evt instanceof ResponseEvent) {
623                    dialog = ((ResponseEvent) evt).getDialog();
624                }
625                if (dialog != null) mDialog = dialog;
626
627                boolean processed;
628
629                switch (mState) {
630                case SipSession.State.REGISTERING:
631                case SipSession.State.DEREGISTERING:
632                    processed = registeringToReady(evt);
633                    break;
634                case SipSession.State.PINGING:
635                    processed = keepAliveProcess(evt);
636                    break;
637                case SipSession.State.READY_TO_CALL:
638                    processed = readyForCall(evt);
639                    break;
640                case SipSession.State.INCOMING_CALL:
641                    processed = incomingCall(evt);
642                    break;
643                case SipSession.State.INCOMING_CALL_ANSWERING:
644                    processed = incomingCallToInCall(evt);
645                    break;
646                case SipSession.State.OUTGOING_CALL:
647                case SipSession.State.OUTGOING_CALL_RING_BACK:
648                    processed = outgoingCall(evt);
649                    break;
650                case SipSession.State.OUTGOING_CALL_CANCELING:
651                    processed = outgoingCallToReady(evt);
652                    break;
653                case SipSession.State.IN_CALL:
654                    processed = inCall(evt);
655                    break;
656                default:
657                    processed = false;
658                }
659                return (processed || processExceptions(evt));
660            }
661        }
662
663        private boolean processExceptions(EventObject evt) throws SipException {
664            if (isRequestEvent(Request.BYE, evt)) {
665                // terminate the call whenever a BYE is received
666                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
667                endCallNormally();
668                return true;
669            } else if (isRequestEvent(Request.CANCEL, evt)) {
670                mSipHelper.sendResponse((RequestEvent) evt,
671                        Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST);
672                return true;
673            } else if (evt instanceof TransactionTerminatedEvent) {
674                if (isCurrentTransaction((TransactionTerminatedEvent) evt)) {
675                    if (evt instanceof TimeoutEvent) {
676                        processTimeout((TimeoutEvent) evt);
677                    } else {
678                        processTransactionTerminated(
679                                (TransactionTerminatedEvent) evt);
680                    }
681                    return true;
682                }
683            } else if (isRequestEvent(Request.OPTIONS, evt)) {
684                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
685                return true;
686            } else if (evt instanceof DialogTerminatedEvent) {
687                processDialogTerminated((DialogTerminatedEvent) evt);
688                return true;
689            }
690            return false;
691        }
692
693        private void processDialogTerminated(DialogTerminatedEvent event) {
694            if (mDialog == event.getDialog()) {
695                onError(new SipException("dialog terminated"));
696            } else {
697                Log.d(TAG, "not the current dialog; current=" + mDialog
698                        + ", terminated=" + event.getDialog());
699            }
700        }
701
702        private boolean isCurrentTransaction(TransactionTerminatedEvent event) {
703            Transaction current = event.isServerTransaction()
704                    ? mServerTransaction
705                    : mClientTransaction;
706            Transaction target = event.isServerTransaction()
707                    ? event.getServerTransaction()
708                    : event.getClientTransaction();
709
710            if ((current != target) && (mState != SipSession.State.PINGING)) {
711                Log.d(TAG, "not the current transaction; current="
712                        + toString(current) + ", target=" + toString(target));
713                return false;
714            } else if (current != null) {
715                Log.d(TAG, "transaction terminated: " + toString(current));
716                return true;
717            } else {
718                // no transaction; shouldn't be here; ignored
719                return true;
720            }
721        }
722
723        private String toString(Transaction transaction) {
724            if (transaction == null) return "null";
725            Request request = transaction.getRequest();
726            Dialog dialog = transaction.getDialog();
727            CSeqHeader cseq = (CSeqHeader) request.getHeader(CSeqHeader.NAME);
728            return String.format("req=%s,%s,s=%s,ds=%s,", request.getMethod(),
729                    cseq.getSeqNumber(), transaction.getState(),
730                    ((dialog == null) ? "-" : dialog.getState()));
731        }
732
733        private void processTransactionTerminated(
734                TransactionTerminatedEvent event) {
735            switch (mState) {
736                case SipSession.State.IN_CALL:
737                case SipSession.State.READY_TO_CALL:
738                    Log.d(TAG, "Transaction terminated; do nothing");
739                    break;
740                default:
741                    Log.d(TAG, "Transaction terminated early: " + this);
742                    onError(SipErrorCode.TRANSACTION_TERMINTED,
743                            "transaction terminated");
744            }
745        }
746
747        private void processTimeout(TimeoutEvent event) {
748            Log.d(TAG, "processing Timeout...");
749            switch (mState) {
750                case SipSession.State.REGISTERING:
751                case SipSession.State.DEREGISTERING:
752                    reset();
753                    mProxy.onRegistrationTimeout(this);
754                    break;
755                case SipSession.State.INCOMING_CALL:
756                case SipSession.State.INCOMING_CALL_ANSWERING:
757                case SipSession.State.OUTGOING_CALL:
758                case SipSession.State.OUTGOING_CALL_CANCELING:
759                    onError(SipErrorCode.TIME_OUT, event.toString());
760                    break;
761                case SipSession.State.PINGING:
762                    reset();
763                    mReRegisterFlag = true;
764                    break;
765
766                default:
767                    Log.d(TAG, "   do nothing");
768                    break;
769            }
770        }
771
772        private int getExpiryTime(Response response) {
773            int expires = EXPIRY_TIME;
774            ExpiresHeader expiresHeader = (ExpiresHeader)
775                    response.getHeader(ExpiresHeader.NAME);
776            if (expiresHeader != null) expires = expiresHeader.getExpires();
777            expiresHeader = (ExpiresHeader)
778                    response.getHeader(MinExpiresHeader.NAME);
779            if (expiresHeader != null) {
780                expires = Math.max(expires, expiresHeader.getExpires());
781            }
782            return expires;
783        }
784
785        private boolean keepAliveProcess(EventObject evt) throws SipException {
786            if (evt instanceof OptionsCommand) {
787                mClientTransaction = mSipHelper.sendKeepAlive(mLocalProfile,
788                        generateTag());
789                mDialog = mClientTransaction.getDialog();
790                addSipSession(this);
791                return true;
792            } else if (evt instanceof ResponseEvent) {
793                return parseOptionsResult(evt);
794            }
795            return false;
796        }
797
798        private boolean parseOptionsResult(EventObject evt) {
799            if (expectResponse(Request.OPTIONS, evt)) {
800                ResponseEvent event = (ResponseEvent) evt;
801                int rPort = getRPortFromResponse(event.getResponse());
802                if (rPort != -1) {
803                    if (mRPort == 0) mRPort = rPort;
804                    if (mRPort != rPort) {
805                        mReRegisterFlag = true;
806                        if (DEBUG) Log.w(TAG, String.format(
807                                "rport is changed: %d <> %d", mRPort, rPort));
808                        mRPort = rPort;
809                    } else {
810                        if (DEBUG_PING) Log.w(TAG, "rport is the same: " + rPort);
811                    }
812                } else {
813                    if (DEBUG) Log.w(TAG, "peer did not respond rport");
814                }
815                reset();
816                return true;
817            }
818            return false;
819        }
820
821        private int getRPortFromResponse(Response response) {
822            ViaHeader viaHeader = (ViaHeader)(response.getHeader(
823                    SIPHeaderNames.VIA));
824            return (viaHeader == null) ? -1 : viaHeader.getRPort();
825        }
826
827        private boolean registeringToReady(EventObject evt)
828                throws SipException {
829            if (expectResponse(Request.REGISTER, evt)) {
830                ResponseEvent event = (ResponseEvent) evt;
831                Response response = event.getResponse();
832
833                int statusCode = response.getStatusCode();
834                switch (statusCode) {
835                case Response.OK:
836                    int state = mState;
837                    onRegistrationDone((state == SipSession.State.REGISTERING)
838                            ? getExpiryTime(((ResponseEvent) evt).getResponse())
839                            : -1);
840                    return true;
841                case Response.UNAUTHORIZED:
842                case Response.PROXY_AUTHENTICATION_REQUIRED:
843                    handleAuthentication(event);
844                    return true;
845                default:
846                    if (statusCode >= 500) {
847                        onRegistrationFailed(response);
848                        return true;
849                    }
850                }
851            }
852            return false;
853        }
854
855        private boolean handleAuthentication(ResponseEvent event)
856                throws SipException {
857            Response response = event.getResponse();
858            String nonce = getNonceFromResponse(response);
859            if (nonce == null) {
860                onError(SipErrorCode.SERVER_ERROR,
861                        "server does not provide challenge");
862                return false;
863            } else if (mAuthenticationRetryCount < 2) {
864                mClientTransaction = mSipHelper.handleChallenge(
865                        event, getAccountManager());
866                mDialog = mClientTransaction.getDialog();
867                mAuthenticationRetryCount++;
868                if (isLoggable(this, event)) {
869                    Log.d(TAG, "   authentication retry count="
870                            + mAuthenticationRetryCount);
871                }
872                return true;
873            } else {
874                onError(SipErrorCode.INVALID_CREDENTIALS,
875                        "incorrect username or password");
876                return false;
877            }
878        }
879
880        private boolean crossDomainAuthenticationRequired(Response response) {
881            String realm = getRealmFromResponse(response);
882            if (realm == null) realm = "";
883            return !mLocalProfile.getSipDomain().trim().equals(realm.trim());
884        }
885
886        private AccountManager getAccountManager() {
887            return new AccountManager() {
888                public UserCredentials getCredentials(ClientTransaction
889                        challengedTransaction, String realm) {
890                    return new UserCredentials() {
891                        public String getUserName() {
892                            return mLocalProfile.getUserName();
893                        }
894
895                        public String getPassword() {
896                            return mPassword;
897                        }
898
899                        public String getSipDomain() {
900                            return mLocalProfile.getSipDomain();
901                        }
902                    };
903                }
904            };
905        }
906
907        private String getRealmFromResponse(Response response) {
908            WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
909                    SIPHeaderNames.WWW_AUTHENTICATE);
910            if (wwwAuth != null) return wwwAuth.getRealm();
911            ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
912                    SIPHeaderNames.PROXY_AUTHENTICATE);
913            return (proxyAuth == null) ? null : proxyAuth.getRealm();
914        }
915
916        private String getNonceFromResponse(Response response) {
917            WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
918                    SIPHeaderNames.WWW_AUTHENTICATE);
919            if (wwwAuth != null) return wwwAuth.getNonce();
920            ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
921                    SIPHeaderNames.PROXY_AUTHENTICATE);
922            return (proxyAuth == null) ? null : proxyAuth.getNonce();
923        }
924
925        private boolean readyForCall(EventObject evt) throws SipException {
926            // expect MakeCallCommand, RegisterCommand, DEREGISTER
927            if (evt instanceof MakeCallCommand) {
928                mState = SipSession.State.OUTGOING_CALL;
929                MakeCallCommand cmd = (MakeCallCommand) evt;
930                mPeerProfile = cmd.getPeerProfile();
931                mClientTransaction = mSipHelper.sendInvite(mLocalProfile,
932                        mPeerProfile, cmd.getSessionDescription(),
933                        generateTag());
934                mDialog = mClientTransaction.getDialog();
935                addSipSession(this);
936                startSessionTimer(cmd.getTimeout());
937                mProxy.onCalling(this);
938                return true;
939            } else if (evt instanceof RegisterCommand) {
940                mState = SipSession.State.REGISTERING;
941                int duration = ((RegisterCommand) evt).getDuration();
942                mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
943                        generateTag(), duration);
944                mDialog = mClientTransaction.getDialog();
945                addSipSession(this);
946                mProxy.onRegistering(this);
947                return true;
948            } else if (DEREGISTER == evt) {
949                mState = SipSession.State.DEREGISTERING;
950                mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
951                        generateTag(), 0);
952                mDialog = mClientTransaction.getDialog();
953                addSipSession(this);
954                mProxy.onRegistering(this);
955                return true;
956            }
957            return false;
958        }
959
960        private boolean incomingCall(EventObject evt) throws SipException {
961            // expect MakeCallCommand(answering) , END_CALL cmd , Cancel
962            if (evt instanceof MakeCallCommand) {
963                // answer call
964                mState = SipSession.State.INCOMING_CALL_ANSWERING;
965                mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived,
966                        mLocalProfile,
967                        ((MakeCallCommand) evt).getSessionDescription(),
968                        mServerTransaction);
969                startSessionTimer(((MakeCallCommand) evt).getTimeout());
970                return true;
971            } else if (END_CALL == evt) {
972                mSipHelper.sendInviteBusyHere(mInviteReceived,
973                        mServerTransaction);
974                endCallNormally();
975                return true;
976            } else if (isRequestEvent(Request.CANCEL, evt)) {
977                RequestEvent event = (RequestEvent) evt;
978                mSipHelper.sendResponse(event, Response.OK);
979                mSipHelper.sendInviteRequestTerminated(
980                        mInviteReceived.getRequest(), mServerTransaction);
981                endCallNormally();
982                return true;
983            }
984            return false;
985        }
986
987        private boolean incomingCallToInCall(EventObject evt)
988                throws SipException {
989            // expect ACK, CANCEL request
990            if (isRequestEvent(Request.ACK, evt)) {
991                establishCall();
992                return true;
993            } else if (isRequestEvent(Request.CANCEL, evt)) {
994                // http://tools.ietf.org/html/rfc3261#section-9.2
995                // Final response has been sent; do nothing here.
996                return true;
997            }
998            return false;
999        }
1000
1001        private boolean outgoingCall(EventObject evt) throws SipException {
1002            if (expectResponse(Request.INVITE, evt)) {
1003                ResponseEvent event = (ResponseEvent) evt;
1004                Response response = event.getResponse();
1005
1006                int statusCode = response.getStatusCode();
1007                switch (statusCode) {
1008                case Response.RINGING:
1009                case Response.CALL_IS_BEING_FORWARDED:
1010                case Response.QUEUED:
1011                case Response.SESSION_PROGRESS:
1012                    // feedback any provisional responses (except TRYING) as
1013                    // ring back for better UX
1014                    if (mState == SipSession.State.OUTGOING_CALL) {
1015                        mState = SipSession.State.OUTGOING_CALL_RING_BACK;
1016                        cancelSessionTimer();
1017                        mProxy.onRingingBack(this);
1018                    }
1019                    return true;
1020                case Response.OK:
1021                    mSipHelper.sendInviteAck(event, mDialog);
1022                    mPeerSessionDescription = extractContent(response);
1023                    establishCall();
1024                    return true;
1025                case Response.UNAUTHORIZED:
1026                case Response.PROXY_AUTHENTICATION_REQUIRED:
1027                    if (crossDomainAuthenticationRequired(response)) {
1028                        onError(SipErrorCode.CROSS_DOMAIN_AUTHENTICATION,
1029                                getRealmFromResponse(response));
1030                    } else 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 fallbackToPreviousInCall(int errorCode, String message) {
1166            mState = SipSession.State.IN_CALL;
1167            mProxy.onCallChangeFailed(this, errorCode, message);
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                    if ((errorCode != SipErrorCode.DATA_CONNECTION_LOST)
1194                            && mInCall) {
1195                        fallbackToPreviousInCall(errorCode, message);
1196                    } else {
1197                        endCallOnError(errorCode, message);
1198                    }
1199            }
1200        }
1201
1202
1203        private void onError(Throwable exception) {
1204            exception = getRootCause(exception);
1205            onError(getErrorCode(exception), exception.toString());
1206        }
1207
1208        private void onError(Response response) {
1209            int statusCode = response.getStatusCode();
1210            if (!mInCall && (statusCode == Response.BUSY_HERE)) {
1211                endCallOnBusy();
1212            } else {
1213                onError(getErrorCode(statusCode), createErrorMessage(response));
1214            }
1215        }
1216
1217        private int getErrorCode(int responseStatusCode) {
1218            switch (responseStatusCode) {
1219                case Response.TEMPORARILY_UNAVAILABLE:
1220                case Response.FORBIDDEN:
1221                case Response.GONE:
1222                case Response.NOT_FOUND:
1223                case Response.NOT_ACCEPTABLE:
1224                case Response.NOT_ACCEPTABLE_HERE:
1225                    return SipErrorCode.PEER_NOT_REACHABLE;
1226
1227                case Response.REQUEST_URI_TOO_LONG:
1228                case Response.ADDRESS_INCOMPLETE:
1229                case Response.AMBIGUOUS:
1230                    return SipErrorCode.INVALID_REMOTE_URI;
1231
1232                case Response.REQUEST_TIMEOUT:
1233                    return SipErrorCode.TIME_OUT;
1234
1235                default:
1236                    if (responseStatusCode < 500) {
1237                        return SipErrorCode.CLIENT_ERROR;
1238                    } else {
1239                        return SipErrorCode.SERVER_ERROR;
1240                    }
1241            }
1242        }
1243
1244        private Throwable getRootCause(Throwable exception) {
1245            Throwable cause = exception.getCause();
1246            while (cause != null) {
1247                exception = cause;
1248                cause = exception.getCause();
1249            }
1250            return exception;
1251        }
1252
1253        private int getErrorCode(Throwable exception) {
1254            String message = exception.getMessage();
1255            if (exception instanceof UnknownHostException) {
1256                return SipErrorCode.SERVER_UNREACHABLE;
1257            } else if (exception instanceof IOException) {
1258                return SipErrorCode.SOCKET_ERROR;
1259            } else {
1260                return SipErrorCode.CLIENT_ERROR;
1261            }
1262        }
1263
1264        private void onRegistrationDone(int duration) {
1265            reset();
1266            mProxy.onRegistrationDone(this, duration);
1267        }
1268
1269        private void onRegistrationFailed(int errorCode, String message) {
1270            reset();
1271            mProxy.onRegistrationFailed(this, errorCode, message);
1272        }
1273
1274        private void onRegistrationFailed(Throwable exception) {
1275            exception = getRootCause(exception);
1276            onRegistrationFailed(getErrorCode(exception),
1277                    exception.toString());
1278        }
1279
1280        private void onRegistrationFailed(Response response) {
1281            int statusCode = response.getStatusCode();
1282            onRegistrationFailed(getErrorCode(statusCode),
1283                    createErrorMessage(response));
1284        }
1285    }
1286
1287    /**
1288     * @return true if the event is a request event matching the specified
1289     *      method; false otherwise
1290     */
1291    private static boolean isRequestEvent(String method, EventObject event) {
1292        try {
1293            if (event instanceof RequestEvent) {
1294                RequestEvent requestEvent = (RequestEvent) event;
1295                return method.equals(requestEvent.getRequest().getMethod());
1296            }
1297        } catch (Throwable e) {
1298        }
1299        return false;
1300    }
1301
1302    private static String getCseqMethod(Message message) {
1303        return ((CSeqHeader) message.getHeader(CSeqHeader.NAME)).getMethod();
1304    }
1305
1306    /**
1307     * @return true if the event is a response event and the CSeqHeader method
1308     * match the given arguments; false otherwise
1309     */
1310    private static boolean expectResponse(
1311            String expectedMethod, EventObject evt) {
1312        if (evt instanceof ResponseEvent) {
1313            ResponseEvent event = (ResponseEvent) evt;
1314            Response response = event.getResponse();
1315            return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
1316        }
1317        return false;
1318    }
1319
1320    /**
1321     * @return true if the event is a response event and the response code and
1322     *      CSeqHeader method match the given arguments; false otherwise
1323     */
1324    private static boolean expectResponse(
1325            int responseCode, String expectedMethod, EventObject evt) {
1326        if (evt instanceof ResponseEvent) {
1327            ResponseEvent event = (ResponseEvent) evt;
1328            Response response = event.getResponse();
1329            if (response.getStatusCode() == responseCode) {
1330                return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
1331            }
1332        }
1333        return false;
1334    }
1335
1336    private static SipProfile createPeerProfile(Request request)
1337            throws SipException {
1338        try {
1339            FromHeader fromHeader =
1340                    (FromHeader) request.getHeader(FromHeader.NAME);
1341            Address address = fromHeader.getAddress();
1342            SipURI uri = (SipURI) address.getURI();
1343            String username = uri.getUser();
1344            if (username == null) username = ANONYMOUS;
1345            return new SipProfile.Builder(username, uri.getHost())
1346                    .setPort(uri.getPort())
1347                    .setDisplayName(address.getDisplayName())
1348                    .build();
1349        } catch (IllegalArgumentException e) {
1350            throw new SipException("createPeerProfile()", e);
1351        } catch (ParseException e) {
1352            throw new SipException("createPeerProfile()", e);
1353        }
1354    }
1355
1356    private static boolean isLoggable(SipSessionImpl s) {
1357        if (s != null) {
1358            switch (s.mState) {
1359                case SipSession.State.PINGING:
1360                    return DEBUG_PING;
1361            }
1362        }
1363        return DEBUG;
1364    }
1365
1366    private static boolean isLoggable(EventObject evt) {
1367        return isLoggable(null, evt);
1368    }
1369
1370    private static boolean isLoggable(SipSessionImpl s, EventObject evt) {
1371        if (!isLoggable(s)) return false;
1372        if (evt == null) return false;
1373
1374        if (evt instanceof OptionsCommand) {
1375            return DEBUG_PING;
1376        } else if (evt instanceof ResponseEvent) {
1377            Response response = ((ResponseEvent) evt).getResponse();
1378            if (Request.OPTIONS.equals(response.getHeader(CSeqHeader.NAME))) {
1379                return DEBUG_PING;
1380            }
1381            return DEBUG;
1382        } else if (evt instanceof RequestEvent) {
1383            return DEBUG;
1384        }
1385        return false;
1386    }
1387
1388    private static String log(EventObject evt) {
1389        if (evt instanceof RequestEvent) {
1390            return ((RequestEvent) evt).getRequest().toString();
1391        } else if (evt instanceof ResponseEvent) {
1392            return ((ResponseEvent) evt).getResponse().toString();
1393        } else {
1394            return evt.toString();
1395        }
1396    }
1397
1398    private class OptionsCommand extends EventObject {
1399        public OptionsCommand() {
1400            super(SipSessionGroup.this);
1401        }
1402    }
1403
1404    private class RegisterCommand extends EventObject {
1405        private int mDuration;
1406
1407        public RegisterCommand(int duration) {
1408            super(SipSessionGroup.this);
1409            mDuration = duration;
1410        }
1411
1412        public int getDuration() {
1413            return mDuration;
1414        }
1415    }
1416
1417    private class MakeCallCommand extends EventObject {
1418        private String mSessionDescription;
1419        private int mTimeout; // in seconds
1420
1421        public MakeCallCommand(SipProfile peerProfile,
1422                String sessionDescription) {
1423            this(peerProfile, sessionDescription, -1);
1424        }
1425
1426        public MakeCallCommand(SipProfile peerProfile,
1427                String sessionDescription, int timeout) {
1428            super(peerProfile);
1429            mSessionDescription = sessionDescription;
1430            mTimeout = timeout;
1431        }
1432
1433        public SipProfile getPeerProfile() {
1434            return (SipProfile) getSource();
1435        }
1436
1437        public String getSessionDescription() {
1438            return mSessionDescription;
1439        }
1440
1441        public int getTimeout() {
1442            return mTimeout;
1443        }
1444    }
1445}
1446