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