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 17 package com.android.server.sip; 18 19 import gov.nist.javax.sip.clientauthutils.AccountManager; 20 import gov.nist.javax.sip.clientauthutils.UserCredentials; 21 import gov.nist.javax.sip.header.SIPHeaderNames; 22 import gov.nist.javax.sip.header.ProxyAuthenticate; 23 import gov.nist.javax.sip.header.WWWAuthenticate; 24 import gov.nist.javax.sip.message.SIPMessage; 25 26 import android.net.sip.ISipSession; 27 import android.net.sip.ISipSessionListener; 28 import android.net.sip.SipErrorCode; 29 import android.net.sip.SipProfile; 30 import android.net.sip.SipSession; 31 import android.text.TextUtils; 32 import android.util.Log; 33 34 import java.io.IOException; 35 import java.io.UnsupportedEncodingException; 36 import java.net.DatagramSocket; 37 import java.net.UnknownHostException; 38 import java.text.ParseException; 39 import java.util.Collection; 40 import java.util.EventObject; 41 import java.util.HashMap; 42 import java.util.Map; 43 import java.util.Properties; 44 import java.util.TooManyListenersException; 45 46 import javax.sip.ClientTransaction; 47 import javax.sip.Dialog; 48 import javax.sip.DialogTerminatedEvent; 49 import javax.sip.IOExceptionEvent; 50 import javax.sip.InvalidArgumentException; 51 import javax.sip.ListeningPoint; 52 import javax.sip.ObjectInUseException; 53 import javax.sip.RequestEvent; 54 import javax.sip.ResponseEvent; 55 import javax.sip.ServerTransaction; 56 import javax.sip.SipException; 57 import javax.sip.SipFactory; 58 import javax.sip.SipListener; 59 import javax.sip.SipProvider; 60 import javax.sip.SipStack; 61 import javax.sip.TimeoutEvent; 62 import javax.sip.Transaction; 63 import javax.sip.TransactionState; 64 import javax.sip.TransactionTerminatedEvent; 65 import javax.sip.TransactionUnavailableException; 66 import javax.sip.address.Address; 67 import javax.sip.address.SipURI; 68 import javax.sip.header.CSeqHeader; 69 import javax.sip.header.ExpiresHeader; 70 import javax.sip.header.FromHeader; 71 import javax.sip.header.MinExpiresHeader; 72 import javax.sip.header.ViaHeader; 73 import javax.sip.message.Message; 74 import javax.sip.message.Request; 75 import javax.sip.message.Response; 76 77 /** 78 * Manages {@link ISipSession}'s for a SIP account. 79 */ 80 class 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