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