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.ProxyAuthenticate; 22 import gov.nist.javax.sip.header.ReferTo; 23 import gov.nist.javax.sip.header.SIPHeaderNames; 24 import gov.nist.javax.sip.header.StatusLine; 25 import gov.nist.javax.sip.header.WWWAuthenticate; 26 import gov.nist.javax.sip.header.extensions.ReferredByHeader; 27 import gov.nist.javax.sip.header.extensions.ReplacesHeader; 28 import gov.nist.javax.sip.message.SIPMessage; 29 import gov.nist.javax.sip.message.SIPResponse; 30 31 import android.net.sip.ISipSession; 32 import android.net.sip.ISipSessionListener; 33 import android.net.sip.SipErrorCode; 34 import android.net.sip.SipProfile; 35 import android.net.sip.SipSession; 36 import android.net.sip.SipSessionAdapter; 37 import android.text.TextUtils; 38 import android.util.Log; 39 40 import java.io.IOException; 41 import java.io.UnsupportedEncodingException; 42 import java.net.DatagramSocket; 43 import java.net.UnknownHostException; 44 import java.text.ParseException; 45 import java.util.Collection; 46 import java.util.EventObject; 47 import java.util.HashMap; 48 import java.util.Map; 49 import java.util.Properties; 50 import java.util.TooManyListenersException; 51 52 import javax.sip.ClientTransaction; 53 import javax.sip.Dialog; 54 import javax.sip.DialogTerminatedEvent; 55 import javax.sip.IOExceptionEvent; 56 import javax.sip.InvalidArgumentException; 57 import javax.sip.ListeningPoint; 58 import javax.sip.ObjectInUseException; 59 import javax.sip.RequestEvent; 60 import javax.sip.ResponseEvent; 61 import javax.sip.ServerTransaction; 62 import javax.sip.SipException; 63 import javax.sip.SipFactory; 64 import javax.sip.SipListener; 65 import javax.sip.SipProvider; 66 import javax.sip.SipStack; 67 import javax.sip.TimeoutEvent; 68 import javax.sip.Transaction; 69 import javax.sip.TransactionState; 70 import javax.sip.TransactionTerminatedEvent; 71 import javax.sip.TransactionUnavailableException; 72 import javax.sip.address.Address; 73 import javax.sip.address.SipURI; 74 import javax.sip.header.CSeqHeader; 75 import javax.sip.header.ContactHeader; 76 import javax.sip.header.ExpiresHeader; 77 import javax.sip.header.FromHeader; 78 import javax.sip.header.HeaderAddress; 79 import javax.sip.header.MinExpiresHeader; 80 import javax.sip.header.ReferToHeader; 81 import javax.sip.header.ViaHeader; 82 import javax.sip.message.Message; 83 import javax.sip.message.Request; 84 import javax.sip.message.Response; 85 86 87 /** 88 * Manages {@link ISipSession}'s for a SIP account. 89 */ 90 class SipSessionGroup implements SipListener { 91 private static final String TAG = "SipSession"; 92 private static final boolean DEBUG = false; 93 private static final boolean DEBUG_PING = false; 94 private static final String ANONYMOUS = "anonymous"; 95 // Limit the size of thread pool to 1 for the order issue when the phone is 96 // waken up from sleep and there are many packets to be processed in the SIP 97 // stack. Note: The default thread pool size in NIST SIP stack is -1 which is 98 // unlimited. 99 private static final String THREAD_POOL_SIZE = "1"; 100 private static final int EXPIRY_TIME = 3600; // in seconds 101 private static final int CANCEL_CALL_TIMER = 3; // in seconds 102 private static final int END_CALL_TIMER = 3; // in seconds 103 private static final int KEEPALIVE_TIMEOUT = 5; // in seconds 104 private static final int INCALL_KEEPALIVE_INTERVAL = 10; // in seconds 105 private static final long WAKE_LOCK_HOLDING_TIME = 500; // in milliseconds 106 107 private static final EventObject DEREGISTER = new EventObject("Deregister"); 108 private static final EventObject END_CALL = new EventObject("End call"); 109 private static final EventObject HOLD_CALL = new EventObject("Hold call"); 110 private static final EventObject CONTINUE_CALL 111 = new EventObject("Continue call"); 112 113 private final SipProfile mLocalProfile; 114 private final String mPassword; 115 116 private SipStack mSipStack; 117 private SipHelper mSipHelper; 118 119 // session that processes INVITE requests 120 private SipSessionImpl mCallReceiverSession; 121 private String mLocalIp; 122 123 private SipWakeupTimer mWakeupTimer; 124 private SipWakeLock mWakeLock; 125 126 // call-id-to-SipSession map 127 private Map<String, SipSessionImpl> mSessionMap = 128 new HashMap<String, SipSessionImpl>(); 129 130 // external address observed from any response 131 private String mExternalIp; 132 private int mExternalPort; 133 134 /** 135 * @param myself the local profile with password crossed out 136 * @param password the password of the profile 137 * @throws IOException if cannot assign requested address 138 */ 139 public SipSessionGroup(String localIp, SipProfile myself, String password, 140 SipWakeupTimer timer, SipWakeLock wakeLock) throws SipException, 141 IOException { 142 mLocalProfile = myself; 143 mPassword = password; 144 mWakeupTimer = timer; 145 mWakeLock = wakeLock; 146 reset(localIp); 147 } 148 149 // TODO: remove this method once SipWakeupTimer can better handle variety 150 // of timeout values 151 void setWakeupTimer(SipWakeupTimer timer) { 152 mWakeupTimer = timer; 153 } 154 155 synchronized void reset(String localIp) throws SipException, IOException { 156 mLocalIp = localIp; 157 if (localIp == null) return; 158 159 SipProfile myself = mLocalProfile; 160 SipFactory sipFactory = SipFactory.getInstance(); 161 Properties properties = new Properties(); 162 properties.setProperty("javax.sip.STACK_NAME", getStackName()); 163 properties.setProperty( 164 "gov.nist.javax.sip.THREAD_POOL_SIZE", THREAD_POOL_SIZE); 165 String outboundProxy = myself.getProxyAddress(); 166 if (!TextUtils.isEmpty(outboundProxy)) { 167 Log.v(TAG, "outboundProxy is " + outboundProxy); 168 properties.setProperty("javax.sip.OUTBOUND_PROXY", outboundProxy 169 + ":" + myself.getPort() + "/" + myself.getProtocol()); 170 } 171 SipStack stack = mSipStack = sipFactory.createSipStack(properties); 172 173 try { 174 SipProvider provider = stack.createSipProvider( 175 stack.createListeningPoint(localIp, allocateLocalPort(), 176 myself.getProtocol())); 177 provider.addSipListener(this); 178 mSipHelper = new SipHelper(stack, provider); 179 } catch (InvalidArgumentException e) { 180 throw new IOException(e.getMessage()); 181 } catch (TooManyListenersException e) { 182 // must never happen 183 throw new SipException("SipSessionGroup constructor", e); 184 } 185 Log.d(TAG, " start stack for " + myself.getUriString()); 186 stack.start(); 187 188 mCallReceiverSession = null; 189 mSessionMap.clear(); 190 191 resetExternalAddress(); 192 } 193 194 synchronized void onConnectivityChanged() { 195 SipSessionImpl[] ss = mSessionMap.values().toArray( 196 new SipSessionImpl[mSessionMap.size()]); 197 // Iterate on the copied array instead of directly on mSessionMap to 198 // avoid ConcurrentModificationException being thrown when 199 // SipSessionImpl removes itself from mSessionMap in onError() in the 200 // following loop. 201 for (SipSessionImpl s : ss) { 202 s.onError(SipErrorCode.DATA_CONNECTION_LOST, 203 "data connection lost"); 204 } 205 } 206 207 synchronized void resetExternalAddress() { 208 if (DEBUG) { 209 Log.d(TAG, " reset external addr on " + mSipStack); 210 } 211 mExternalIp = null; 212 mExternalPort = 0; 213 } 214 215 public SipProfile getLocalProfile() { 216 return mLocalProfile; 217 } 218 219 public String getLocalProfileUri() { 220 return mLocalProfile.getUriString(); 221 } 222 223 private String getStackName() { 224 return "stack" + System.currentTimeMillis(); 225 } 226 227 public synchronized void close() { 228 Log.d(TAG, " close stack for " + mLocalProfile.getUriString()); 229 onConnectivityChanged(); 230 mSessionMap.clear(); 231 closeToNotReceiveCalls(); 232 if (mSipStack != null) { 233 mSipStack.stop(); 234 mSipStack = null; 235 mSipHelper = null; 236 } 237 } 238 239 public synchronized boolean isClosed() { 240 return (mSipStack == null); 241 } 242 243 // For internal use, require listener not to block in callbacks. 244 public synchronized void openToReceiveCalls(ISipSessionListener listener) { 245 if (mCallReceiverSession == null) { 246 mCallReceiverSession = new SipSessionCallReceiverImpl(listener); 247 } else { 248 mCallReceiverSession.setListener(listener); 249 } 250 } 251 252 public synchronized void closeToNotReceiveCalls() { 253 mCallReceiverSession = null; 254 } 255 256 public ISipSession createSession(ISipSessionListener listener) { 257 return (isClosed() ? null : new SipSessionImpl(listener)); 258 } 259 260 private static int allocateLocalPort() throws SipException { 261 try { 262 DatagramSocket s = new DatagramSocket(); 263 int localPort = s.getLocalPort(); 264 s.close(); 265 return localPort; 266 } catch (IOException e) { 267 throw new SipException("allocateLocalPort()", e); 268 } 269 } 270 271 synchronized boolean containsSession(String callId) { 272 return mSessionMap.containsKey(callId); 273 } 274 275 private synchronized SipSessionImpl getSipSession(EventObject event) { 276 String key = SipHelper.getCallId(event); 277 SipSessionImpl session = mSessionMap.get(key); 278 if ((session != null) && isLoggable(session)) { 279 Log.d(TAG, "session key from event: " + key); 280 Log.d(TAG, "active sessions:"); 281 for (String k : mSessionMap.keySet()) { 282 Log.d(TAG, " ..." + k + ": " + mSessionMap.get(k)); 283 } 284 } 285 return ((session != null) ? session : mCallReceiverSession); 286 } 287 288 private synchronized void addSipSession(SipSessionImpl newSession) { 289 removeSipSession(newSession); 290 String key = newSession.getCallId(); 291 mSessionMap.put(key, newSession); 292 if (isLoggable(newSession)) { 293 Log.d(TAG, "+++ add a session with key: '" + key + "'"); 294 for (String k : mSessionMap.keySet()) { 295 Log.d(TAG, " " + k + ": " + mSessionMap.get(k)); 296 } 297 } 298 } 299 300 private synchronized void removeSipSession(SipSessionImpl session) { 301 if (session == mCallReceiverSession) return; 302 String key = session.getCallId(); 303 SipSessionImpl s = mSessionMap.remove(key); 304 // sanity check 305 if ((s != null) && (s != session)) { 306 Log.w(TAG, "session " + session + " is not associated with key '" 307 + key + "'"); 308 mSessionMap.put(key, s); 309 for (Map.Entry<String, SipSessionImpl> entry 310 : mSessionMap.entrySet()) { 311 if (entry.getValue() == s) { 312 key = entry.getKey(); 313 mSessionMap.remove(key); 314 } 315 } 316 } 317 318 if ((s != null) && isLoggable(s)) { 319 Log.d(TAG, "remove session " + session + " @key '" + key + "'"); 320 for (String k : mSessionMap.keySet()) { 321 Log.d(TAG, " " + k + ": " + mSessionMap.get(k)); 322 } 323 } 324 } 325 326 public void processRequest(final RequestEvent event) { 327 if (isRequestEvent(Request.INVITE, event)) { 328 if (DEBUG) Log.d(TAG, "<<<<< got INVITE, thread:" 329 + Thread.currentThread()); 330 // Acquire a wake lock and keep it for WAKE_LOCK_HOLDING_TIME; 331 // should be large enough to bring up the app. 332 mWakeLock.acquire(WAKE_LOCK_HOLDING_TIME); 333 } 334 process(event); 335 } 336 337 public void processResponse(ResponseEvent event) { 338 process(event); 339 } 340 341 public void processIOException(IOExceptionEvent event) { 342 process(event); 343 } 344 345 public void processTimeout(TimeoutEvent event) { 346 process(event); 347 } 348 349 public void processTransactionTerminated(TransactionTerminatedEvent event) { 350 process(event); 351 } 352 353 public void processDialogTerminated(DialogTerminatedEvent event) { 354 process(event); 355 } 356 357 private synchronized void process(EventObject event) { 358 SipSessionImpl session = getSipSession(event); 359 try { 360 boolean isLoggable = isLoggable(session, event); 361 boolean processed = (session != null) && session.process(event); 362 if (isLoggable && processed) { 363 Log.d(TAG, "new state after: " 364 + SipSession.State.toString(session.mState)); 365 } 366 } catch (Throwable e) { 367 Log.w(TAG, "event process error: " + event, getRootCause(e)); 368 session.onError(e); 369 } 370 } 371 372 private String extractContent(Message message) { 373 // Currently we do not support secure MIME bodies. 374 byte[] bytes = message.getRawContent(); 375 if (bytes != null) { 376 try { 377 if (message instanceof SIPMessage) { 378 return ((SIPMessage) message).getMessageContent(); 379 } else { 380 return new String(bytes, "UTF-8"); 381 } 382 } catch (UnsupportedEncodingException e) { 383 } 384 } 385 return null; 386 } 387 388 private void extractExternalAddress(ResponseEvent evt) { 389 Response response = evt.getResponse(); 390 ViaHeader viaHeader = (ViaHeader)(response.getHeader( 391 SIPHeaderNames.VIA)); 392 if (viaHeader == null) return; 393 int rport = viaHeader.getRPort(); 394 String externalIp = viaHeader.getReceived(); 395 if ((rport > 0) && (externalIp != null)) { 396 mExternalIp = externalIp; 397 mExternalPort = rport; 398 if (DEBUG) { 399 Log.d(TAG, " got external addr " + externalIp + ":" + rport 400 + " on " + mSipStack); 401 } 402 } 403 } 404 405 private Throwable getRootCause(Throwable exception) { 406 Throwable cause = exception.getCause(); 407 while (cause != null) { 408 exception = cause; 409 cause = exception.getCause(); 410 } 411 return exception; 412 } 413 414 private SipSessionImpl createNewSession(RequestEvent event, 415 ISipSessionListener listener, ServerTransaction transaction, 416 int newState) throws SipException { 417 SipSessionImpl newSession = new SipSessionImpl(listener); 418 newSession.mServerTransaction = transaction; 419 newSession.mState = newState; 420 newSession.mDialog = newSession.mServerTransaction.getDialog(); 421 newSession.mInviteReceived = event; 422 newSession.mPeerProfile = createPeerProfile((HeaderAddress) 423 event.getRequest().getHeader(FromHeader.NAME)); 424 newSession.mPeerSessionDescription = 425 extractContent(event.getRequest()); 426 return newSession; 427 } 428 429 private class SipSessionCallReceiverImpl extends SipSessionImpl { 430 public SipSessionCallReceiverImpl(ISipSessionListener listener) { 431 super(listener); 432 } 433 434 private int processInviteWithReplaces(RequestEvent event, 435 ReplacesHeader replaces) { 436 String callId = replaces.getCallId(); 437 SipSessionImpl session = mSessionMap.get(callId); 438 if (session == null) { 439 return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST; 440 } 441 442 Dialog dialog = session.mDialog; 443 if (dialog == null) return Response.DECLINE; 444 445 if (!dialog.getLocalTag().equals(replaces.getToTag()) || 446 !dialog.getRemoteTag().equals(replaces.getFromTag())) { 447 // No match is found, returns 481. 448 return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST; 449 } 450 451 ReferredByHeader referredBy = (ReferredByHeader) event.getRequest() 452 .getHeader(ReferredByHeader.NAME); 453 if ((referredBy == null) || 454 !dialog.getRemoteParty().equals(referredBy.getAddress())) { 455 return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST; 456 } 457 return Response.OK; 458 } 459 460 private void processNewInviteRequest(RequestEvent event) 461 throws SipException { 462 ReplacesHeader replaces = (ReplacesHeader) event.getRequest() 463 .getHeader(ReplacesHeader.NAME); 464 SipSessionImpl newSession = null; 465 if (replaces != null) { 466 int response = processInviteWithReplaces(event, replaces); 467 if (DEBUG) { 468 Log.v(TAG, "ReplacesHeader: " + replaces 469 + " response=" + response); 470 } 471 if (response == Response.OK) { 472 SipSessionImpl replacedSession = 473 mSessionMap.get(replaces.getCallId()); 474 // got INVITE w/ replaces request. 475 newSession = createNewSession(event, 476 replacedSession.mProxy.getListener(), 477 mSipHelper.getServerTransaction(event), 478 SipSession.State.INCOMING_CALL); 479 newSession.mProxy.onCallTransferring(newSession, 480 newSession.mPeerSessionDescription); 481 } else { 482 mSipHelper.sendResponse(event, response); 483 } 484 } else { 485 // New Incoming call. 486 newSession = createNewSession(event, mProxy, 487 mSipHelper.sendRinging(event, generateTag()), 488 SipSession.State.INCOMING_CALL); 489 mProxy.onRinging(newSession, newSession.mPeerProfile, 490 newSession.mPeerSessionDescription); 491 } 492 if (newSession != null) addSipSession(newSession); 493 } 494 495 public boolean process(EventObject evt) throws SipException { 496 if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~ " + this + ": " 497 + SipSession.State.toString(mState) + ": processing " 498 + log(evt)); 499 if (isRequestEvent(Request.INVITE, evt)) { 500 processNewInviteRequest((RequestEvent) evt); 501 return true; 502 } else if (isRequestEvent(Request.OPTIONS, evt)) { 503 mSipHelper.sendResponse((RequestEvent) evt, Response.OK); 504 return true; 505 } else { 506 return false; 507 } 508 } 509 } 510 511 static interface KeepAliveProcessCallback { 512 /** Invoked when the response of keeping alive comes back. */ 513 void onResponse(boolean portChanged); 514 void onError(int errorCode, String description); 515 } 516 517 class SipSessionImpl extends ISipSession.Stub { 518 SipProfile mPeerProfile; 519 SipSessionListenerProxy mProxy = new SipSessionListenerProxy(); 520 int mState = SipSession.State.READY_TO_CALL; 521 RequestEvent mInviteReceived; 522 Dialog mDialog; 523 ServerTransaction mServerTransaction; 524 ClientTransaction mClientTransaction; 525 String mPeerSessionDescription; 526 boolean mInCall; 527 SessionTimer mSessionTimer; 528 int mAuthenticationRetryCount; 529 530 private KeepAliveProcess mKeepAliveProcess; 531 532 private SipSessionImpl mKeepAliveSession; 533 534 // the following three members are used for handling refer request. 535 SipSessionImpl mReferSession; 536 ReferredByHeader mReferredBy; 537 String mReplaces; 538 539 // lightweight timer 540 class SessionTimer { 541 private boolean mRunning = true; 542 543 void start(final int timeout) { 544 new Thread(new Runnable() { 545 public void run() { 546 sleep(timeout); 547 if (mRunning) timeout(); 548 } 549 }, "SipSessionTimerThread").start(); 550 } 551 552 synchronized void cancel() { 553 mRunning = false; 554 this.notify(); 555 } 556 557 private void timeout() { 558 synchronized (SipSessionGroup.this) { 559 onError(SipErrorCode.TIME_OUT, "Session timed out!"); 560 } 561 } 562 563 private synchronized void sleep(int timeout) { 564 try { 565 this.wait(timeout * 1000); 566 } catch (InterruptedException e) { 567 Log.e(TAG, "session timer interrupted!"); 568 } 569 } 570 } 571 572 public SipSessionImpl(ISipSessionListener listener) { 573 setListener(listener); 574 } 575 576 SipSessionImpl duplicate() { 577 return new SipSessionImpl(mProxy.getListener()); 578 } 579 580 private void reset() { 581 mInCall = false; 582 removeSipSession(this); 583 mPeerProfile = null; 584 mState = SipSession.State.READY_TO_CALL; 585 mInviteReceived = null; 586 mPeerSessionDescription = null; 587 mAuthenticationRetryCount = 0; 588 mReferSession = null; 589 mReferredBy = null; 590 mReplaces = null; 591 592 if (mDialog != null) mDialog.delete(); 593 mDialog = null; 594 595 try { 596 if (mServerTransaction != null) mServerTransaction.terminate(); 597 } catch (ObjectInUseException e) { 598 // ignored 599 } 600 mServerTransaction = null; 601 602 try { 603 if (mClientTransaction != null) mClientTransaction.terminate(); 604 } catch (ObjectInUseException e) { 605 // ignored 606 } 607 mClientTransaction = null; 608 609 cancelSessionTimer(); 610 611 if (mKeepAliveSession != null) { 612 mKeepAliveSession.stopKeepAliveProcess(); 613 mKeepAliveSession = null; 614 } 615 } 616 617 public boolean isInCall() { 618 return mInCall; 619 } 620 621 public String getLocalIp() { 622 return mLocalIp; 623 } 624 625 public SipProfile getLocalProfile() { 626 return mLocalProfile; 627 } 628 629 public SipProfile getPeerProfile() { 630 return mPeerProfile; 631 } 632 633 public String getCallId() { 634 return SipHelper.getCallId(getTransaction()); 635 } 636 637 private Transaction getTransaction() { 638 if (mClientTransaction != null) return mClientTransaction; 639 if (mServerTransaction != null) return mServerTransaction; 640 return null; 641 } 642 643 public int getState() { 644 return mState; 645 } 646 647 public void setListener(ISipSessionListener listener) { 648 mProxy.setListener((listener instanceof SipSessionListenerProxy) 649 ? ((SipSessionListenerProxy) listener).getListener() 650 : listener); 651 } 652 653 // process the command in a new thread 654 private void doCommandAsync(final EventObject command) { 655 new Thread(new Runnable() { 656 public void run() { 657 try { 658 processCommand(command); 659 } catch (Throwable e) { 660 Log.w(TAG, "command error: " + command + ": " 661 + mLocalProfile.getUriString(), 662 getRootCause(e)); 663 onError(e); 664 } 665 } 666 }, "SipSessionAsyncCmdThread").start(); 667 } 668 669 public void makeCall(SipProfile peerProfile, String sessionDescription, 670 int timeout) { 671 doCommandAsync(new MakeCallCommand(peerProfile, sessionDescription, 672 timeout)); 673 } 674 675 public void answerCall(String sessionDescription, int timeout) { 676 synchronized (SipSessionGroup.this) { 677 if (mPeerProfile == null) return; 678 doCommandAsync(new MakeCallCommand(mPeerProfile, 679 sessionDescription, timeout)); 680 } 681 } 682 683 public void endCall() { 684 doCommandAsync(END_CALL); 685 } 686 687 public void changeCall(String sessionDescription, int timeout) { 688 synchronized (SipSessionGroup.this) { 689 if (mPeerProfile == null) return; 690 doCommandAsync(new MakeCallCommand(mPeerProfile, 691 sessionDescription, timeout)); 692 } 693 } 694 695 public void register(int duration) { 696 doCommandAsync(new RegisterCommand(duration)); 697 } 698 699 public void unregister() { 700 doCommandAsync(DEREGISTER); 701 } 702 703 private void processCommand(EventObject command) throws SipException { 704 if (isLoggable(command)) Log.d(TAG, "process cmd: " + command); 705 if (!process(command)) { 706 onError(SipErrorCode.IN_PROGRESS, 707 "cannot initiate a new transaction to execute: " 708 + command); 709 } 710 } 711 712 protected String generateTag() { 713 // 32-bit randomness 714 return String.valueOf((long) (Math.random() * 0x100000000L)); 715 } 716 717 public String toString() { 718 try { 719 String s = super.toString(); 720 return s.substring(s.indexOf("@")) + ":" 721 + SipSession.State.toString(mState); 722 } catch (Throwable e) { 723 return super.toString(); 724 } 725 } 726 727 public boolean process(EventObject evt) throws SipException { 728 if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~ " + this + ": " 729 + SipSession.State.toString(mState) + ": processing " 730 + log(evt)); 731 synchronized (SipSessionGroup.this) { 732 if (isClosed()) return false; 733 734 if (mKeepAliveProcess != null) { 735 // event consumed by keepalive process 736 if (mKeepAliveProcess.process(evt)) return true; 737 } 738 739 Dialog dialog = null; 740 if (evt instanceof RequestEvent) { 741 dialog = ((RequestEvent) evt).getDialog(); 742 } else if (evt instanceof ResponseEvent) { 743 dialog = ((ResponseEvent) evt).getDialog(); 744 extractExternalAddress((ResponseEvent) evt); 745 } 746 if (dialog != null) mDialog = dialog; 747 748 boolean processed; 749 750 switch (mState) { 751 case SipSession.State.REGISTERING: 752 case SipSession.State.DEREGISTERING: 753 processed = registeringToReady(evt); 754 break; 755 case SipSession.State.READY_TO_CALL: 756 processed = readyForCall(evt); 757 break; 758 case SipSession.State.INCOMING_CALL: 759 processed = incomingCall(evt); 760 break; 761 case SipSession.State.INCOMING_CALL_ANSWERING: 762 processed = incomingCallToInCall(evt); 763 break; 764 case SipSession.State.OUTGOING_CALL: 765 case SipSession.State.OUTGOING_CALL_RING_BACK: 766 processed = outgoingCall(evt); 767 break; 768 case SipSession.State.OUTGOING_CALL_CANCELING: 769 processed = outgoingCallToReady(evt); 770 break; 771 case SipSession.State.IN_CALL: 772 processed = inCall(evt); 773 break; 774 case SipSession.State.ENDING_CALL: 775 processed = endingCall(evt); 776 break; 777 default: 778 processed = false; 779 } 780 return (processed || processExceptions(evt)); 781 } 782 } 783 784 private boolean processExceptions(EventObject evt) throws SipException { 785 if (isRequestEvent(Request.BYE, evt)) { 786 // terminate the call whenever a BYE is received 787 mSipHelper.sendResponse((RequestEvent) evt, Response.OK); 788 endCallNormally(); 789 return true; 790 } else if (isRequestEvent(Request.CANCEL, evt)) { 791 mSipHelper.sendResponse((RequestEvent) evt, 792 Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST); 793 return true; 794 } else if (evt instanceof TransactionTerminatedEvent) { 795 if (isCurrentTransaction((TransactionTerminatedEvent) evt)) { 796 if (evt instanceof TimeoutEvent) { 797 processTimeout((TimeoutEvent) evt); 798 } else { 799 processTransactionTerminated( 800 (TransactionTerminatedEvent) evt); 801 } 802 return true; 803 } 804 } else if (isRequestEvent(Request.OPTIONS, evt)) { 805 mSipHelper.sendResponse((RequestEvent) evt, Response.OK); 806 return true; 807 } else if (evt instanceof DialogTerminatedEvent) { 808 processDialogTerminated((DialogTerminatedEvent) evt); 809 return true; 810 } 811 return false; 812 } 813 814 private void processDialogTerminated(DialogTerminatedEvent event) { 815 if (mDialog == event.getDialog()) { 816 onError(new SipException("dialog terminated")); 817 } else { 818 Log.d(TAG, "not the current dialog; current=" + mDialog 819 + ", terminated=" + event.getDialog()); 820 } 821 } 822 823 private boolean isCurrentTransaction(TransactionTerminatedEvent event) { 824 Transaction current = event.isServerTransaction() 825 ? mServerTransaction 826 : mClientTransaction; 827 Transaction target = event.isServerTransaction() 828 ? event.getServerTransaction() 829 : event.getClientTransaction(); 830 831 if ((current != target) && (mState != SipSession.State.PINGING)) { 832 Log.d(TAG, "not the current transaction; current=" 833 + toString(current) + ", target=" + toString(target)); 834 return false; 835 } else if (current != null) { 836 Log.d(TAG, "transaction terminated: " + toString(current)); 837 return true; 838 } else { 839 // no transaction; shouldn't be here; ignored 840 return true; 841 } 842 } 843 844 private String toString(Transaction transaction) { 845 if (transaction == null) return "null"; 846 Request request = transaction.getRequest(); 847 Dialog dialog = transaction.getDialog(); 848 CSeqHeader cseq = (CSeqHeader) request.getHeader(CSeqHeader.NAME); 849 return String.format("req=%s,%s,s=%s,ds=%s,", request.getMethod(), 850 cseq.getSeqNumber(), transaction.getState(), 851 ((dialog == null) ? "-" : dialog.getState())); 852 } 853 854 private void processTransactionTerminated( 855 TransactionTerminatedEvent event) { 856 switch (mState) { 857 case SipSession.State.IN_CALL: 858 case SipSession.State.READY_TO_CALL: 859 Log.d(TAG, "Transaction terminated; do nothing"); 860 break; 861 default: 862 Log.d(TAG, "Transaction terminated early: " + this); 863 onError(SipErrorCode.TRANSACTION_TERMINTED, 864 "transaction terminated"); 865 } 866 } 867 868 private void processTimeout(TimeoutEvent event) { 869 Log.d(TAG, "processing Timeout..."); 870 switch (mState) { 871 case SipSession.State.REGISTERING: 872 case SipSession.State.DEREGISTERING: 873 reset(); 874 mProxy.onRegistrationTimeout(this); 875 break; 876 case SipSession.State.INCOMING_CALL: 877 case SipSession.State.INCOMING_CALL_ANSWERING: 878 case SipSession.State.OUTGOING_CALL: 879 case SipSession.State.OUTGOING_CALL_CANCELING: 880 onError(SipErrorCode.TIME_OUT, event.toString()); 881 break; 882 883 default: 884 Log.d(TAG, " do nothing"); 885 break; 886 } 887 } 888 889 private int getExpiryTime(Response response) { 890 int time = -1; 891 ContactHeader contact = (ContactHeader) response.getHeader(ContactHeader.NAME); 892 if (contact != null) { 893 time = contact.getExpires(); 894 } 895 ExpiresHeader expires = (ExpiresHeader) response.getHeader(ExpiresHeader.NAME); 896 if (expires != null && (time < 0 || time > expires.getExpires())) { 897 time = expires.getExpires(); 898 } 899 if (time <= 0) { 900 time = EXPIRY_TIME; 901 } 902 expires = (ExpiresHeader) response.getHeader(MinExpiresHeader.NAME); 903 if (expires != null && time < expires.getExpires()) { 904 time = expires.getExpires(); 905 } 906 if (DEBUG) { 907 Log.v(TAG, "Expiry time = " + time); 908 } 909 return time; 910 } 911 912 private boolean registeringToReady(EventObject evt) 913 throws SipException { 914 if (expectResponse(Request.REGISTER, evt)) { 915 ResponseEvent event = (ResponseEvent) evt; 916 Response response = event.getResponse(); 917 918 int statusCode = response.getStatusCode(); 919 switch (statusCode) { 920 case Response.OK: 921 int state = mState; 922 onRegistrationDone((state == SipSession.State.REGISTERING) 923 ? getExpiryTime(((ResponseEvent) evt).getResponse()) 924 : -1); 925 return true; 926 case Response.UNAUTHORIZED: 927 case Response.PROXY_AUTHENTICATION_REQUIRED: 928 handleAuthentication(event); 929 return true; 930 default: 931 if (statusCode >= 500) { 932 onRegistrationFailed(response); 933 return true; 934 } 935 } 936 } 937 return false; 938 } 939 940 private boolean handleAuthentication(ResponseEvent event) 941 throws SipException { 942 Response response = event.getResponse(); 943 String nonce = getNonceFromResponse(response); 944 if (nonce == null) { 945 onError(SipErrorCode.SERVER_ERROR, 946 "server does not provide challenge"); 947 return false; 948 } else if (mAuthenticationRetryCount < 2) { 949 mClientTransaction = mSipHelper.handleChallenge( 950 event, getAccountManager()); 951 mDialog = mClientTransaction.getDialog(); 952 mAuthenticationRetryCount++; 953 if (isLoggable(this, event)) { 954 Log.d(TAG, " authentication retry count=" 955 + mAuthenticationRetryCount); 956 } 957 return true; 958 } else { 959 if (crossDomainAuthenticationRequired(response)) { 960 onError(SipErrorCode.CROSS_DOMAIN_AUTHENTICATION, 961 getRealmFromResponse(response)); 962 } else { 963 onError(SipErrorCode.INVALID_CREDENTIALS, 964 "incorrect username or password"); 965 } 966 return false; 967 } 968 } 969 970 private boolean crossDomainAuthenticationRequired(Response response) { 971 String realm = getRealmFromResponse(response); 972 if (realm == null) realm = ""; 973 return !mLocalProfile.getSipDomain().trim().equals(realm.trim()); 974 } 975 976 private AccountManager getAccountManager() { 977 return new AccountManager() { 978 public UserCredentials getCredentials(ClientTransaction 979 challengedTransaction, String realm) { 980 return new UserCredentials() { 981 public String getUserName() { 982 String username = mLocalProfile.getAuthUserName(); 983 return (!TextUtils.isEmpty(username) ? username : 984 mLocalProfile.getUserName()); 985 } 986 987 public String getPassword() { 988 return mPassword; 989 } 990 991 public String getSipDomain() { 992 return mLocalProfile.getSipDomain(); 993 } 994 }; 995 } 996 }; 997 } 998 999 private String getRealmFromResponse(Response response) { 1000 WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader( 1001 SIPHeaderNames.WWW_AUTHENTICATE); 1002 if (wwwAuth != null) return wwwAuth.getRealm(); 1003 ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader( 1004 SIPHeaderNames.PROXY_AUTHENTICATE); 1005 return (proxyAuth == null) ? null : proxyAuth.getRealm(); 1006 } 1007 1008 private String getNonceFromResponse(Response response) { 1009 WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader( 1010 SIPHeaderNames.WWW_AUTHENTICATE); 1011 if (wwwAuth != null) return wwwAuth.getNonce(); 1012 ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader( 1013 SIPHeaderNames.PROXY_AUTHENTICATE); 1014 return (proxyAuth == null) ? null : proxyAuth.getNonce(); 1015 } 1016 1017 private String getResponseString(int statusCode) { 1018 StatusLine statusLine = new StatusLine(); 1019 statusLine.setStatusCode(statusCode); 1020 statusLine.setReasonPhrase(SIPResponse.getReasonPhrase(statusCode)); 1021 return statusLine.encode(); 1022 } 1023 1024 private boolean readyForCall(EventObject evt) throws SipException { 1025 // expect MakeCallCommand, RegisterCommand, DEREGISTER 1026 if (evt instanceof MakeCallCommand) { 1027 mState = SipSession.State.OUTGOING_CALL; 1028 MakeCallCommand cmd = (MakeCallCommand) evt; 1029 mPeerProfile = cmd.getPeerProfile(); 1030 if (mReferSession != null) { 1031 mSipHelper.sendReferNotify(mReferSession.mDialog, 1032 getResponseString(Response.TRYING)); 1033 } 1034 mClientTransaction = mSipHelper.sendInvite( 1035 mLocalProfile, mPeerProfile, cmd.getSessionDescription(), 1036 generateTag(), mReferredBy, mReplaces); 1037 mDialog = mClientTransaction.getDialog(); 1038 addSipSession(this); 1039 startSessionTimer(cmd.getTimeout()); 1040 mProxy.onCalling(this); 1041 return true; 1042 } else if (evt instanceof RegisterCommand) { 1043 mState = SipSession.State.REGISTERING; 1044 int duration = ((RegisterCommand) evt).getDuration(); 1045 mClientTransaction = mSipHelper.sendRegister(mLocalProfile, 1046 generateTag(), duration); 1047 mDialog = mClientTransaction.getDialog(); 1048 addSipSession(this); 1049 mProxy.onRegistering(this); 1050 return true; 1051 } else if (DEREGISTER == evt) { 1052 mState = SipSession.State.DEREGISTERING; 1053 mClientTransaction = mSipHelper.sendRegister(mLocalProfile, 1054 generateTag(), 0); 1055 mDialog = mClientTransaction.getDialog(); 1056 addSipSession(this); 1057 mProxy.onRegistering(this); 1058 return true; 1059 } 1060 return false; 1061 } 1062 1063 private boolean incomingCall(EventObject evt) throws SipException { 1064 // expect MakeCallCommand(answering) , END_CALL cmd , Cancel 1065 if (evt instanceof MakeCallCommand) { 1066 // answer call 1067 mState = SipSession.State.INCOMING_CALL_ANSWERING; 1068 mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived, 1069 mLocalProfile, 1070 ((MakeCallCommand) evt).getSessionDescription(), 1071 mServerTransaction, 1072 mExternalIp, mExternalPort); 1073 startSessionTimer(((MakeCallCommand) evt).getTimeout()); 1074 return true; 1075 } else if (END_CALL == evt) { 1076 mSipHelper.sendInviteBusyHere(mInviteReceived, 1077 mServerTransaction); 1078 endCallNormally(); 1079 return true; 1080 } else if (isRequestEvent(Request.CANCEL, evt)) { 1081 RequestEvent event = (RequestEvent) evt; 1082 mSipHelper.sendResponse(event, Response.OK); 1083 mSipHelper.sendInviteRequestTerminated( 1084 mInviteReceived.getRequest(), mServerTransaction); 1085 endCallNormally(); 1086 return true; 1087 } 1088 return false; 1089 } 1090 1091 private boolean incomingCallToInCall(EventObject evt) 1092 throws SipException { 1093 // expect ACK, CANCEL request 1094 if (isRequestEvent(Request.ACK, evt)) { 1095 String sdp = extractContent(((RequestEvent) evt).getRequest()); 1096 if (sdp != null) mPeerSessionDescription = sdp; 1097 if (mPeerSessionDescription == null) { 1098 onError(SipErrorCode.CLIENT_ERROR, "peer sdp is empty"); 1099 } else { 1100 establishCall(false); 1101 } 1102 return true; 1103 } else if (isRequestEvent(Request.CANCEL, evt)) { 1104 // http://tools.ietf.org/html/rfc3261#section-9.2 1105 // Final response has been sent; do nothing here. 1106 return true; 1107 } 1108 return false; 1109 } 1110 1111 private boolean outgoingCall(EventObject evt) throws SipException { 1112 if (expectResponse(Request.INVITE, evt)) { 1113 ResponseEvent event = (ResponseEvent) evt; 1114 Response response = event.getResponse(); 1115 1116 int statusCode = response.getStatusCode(); 1117 switch (statusCode) { 1118 case Response.RINGING: 1119 case Response.CALL_IS_BEING_FORWARDED: 1120 case Response.QUEUED: 1121 case Response.SESSION_PROGRESS: 1122 // feedback any provisional responses (except TRYING) as 1123 // ring back for better UX 1124 if (mState == SipSession.State.OUTGOING_CALL) { 1125 mState = SipSession.State.OUTGOING_CALL_RING_BACK; 1126 cancelSessionTimer(); 1127 mProxy.onRingingBack(this); 1128 } 1129 return true; 1130 case Response.OK: 1131 if (mReferSession != null) { 1132 mSipHelper.sendReferNotify(mReferSession.mDialog, 1133 getResponseString(Response.OK)); 1134 // since we don't need to remember the session anymore. 1135 mReferSession = null; 1136 } 1137 mSipHelper.sendInviteAck(event, mDialog); 1138 mPeerSessionDescription = extractContent(response); 1139 establishCall(true); 1140 return true; 1141 case Response.UNAUTHORIZED: 1142 case Response.PROXY_AUTHENTICATION_REQUIRED: 1143 if (handleAuthentication(event)) { 1144 addSipSession(this); 1145 } 1146 return true; 1147 case Response.REQUEST_PENDING: 1148 // TODO: 1149 // rfc3261#section-14.1; re-schedule invite 1150 return true; 1151 default: 1152 if (mReferSession != null) { 1153 mSipHelper.sendReferNotify(mReferSession.mDialog, 1154 getResponseString(Response.SERVICE_UNAVAILABLE)); 1155 } 1156 if (statusCode >= 400) { 1157 // error: an ack is sent automatically by the stack 1158 onError(response); 1159 return true; 1160 } else if (statusCode >= 300) { 1161 // TODO: handle 3xx (redirect) 1162 } else { 1163 return true; 1164 } 1165 } 1166 return false; 1167 } else if (END_CALL == evt) { 1168 // RFC says that UA should not send out cancel when no 1169 // response comes back yet. We are cheating for not checking 1170 // response. 1171 mState = SipSession.State.OUTGOING_CALL_CANCELING; 1172 mSipHelper.sendCancel(mClientTransaction); 1173 startSessionTimer(CANCEL_CALL_TIMER); 1174 return true; 1175 } else if (isRequestEvent(Request.INVITE, evt)) { 1176 // Call self? Send BUSY HERE so server may redirect the call to 1177 // voice mailbox. 1178 RequestEvent event = (RequestEvent) evt; 1179 mSipHelper.sendInviteBusyHere(event, 1180 event.getServerTransaction()); 1181 return true; 1182 } 1183 return false; 1184 } 1185 1186 private boolean outgoingCallToReady(EventObject evt) 1187 throws SipException { 1188 if (evt instanceof ResponseEvent) { 1189 ResponseEvent event = (ResponseEvent) evt; 1190 Response response = event.getResponse(); 1191 int statusCode = response.getStatusCode(); 1192 if (expectResponse(Request.CANCEL, evt)) { 1193 if (statusCode == Response.OK) { 1194 // do nothing; wait for REQUEST_TERMINATED 1195 return true; 1196 } 1197 } else if (expectResponse(Request.INVITE, evt)) { 1198 switch (statusCode) { 1199 case Response.OK: 1200 outgoingCall(evt); // abort Cancel 1201 return true; 1202 case Response.REQUEST_TERMINATED: 1203 endCallNormally(); 1204 return true; 1205 } 1206 } else { 1207 return false; 1208 } 1209 1210 if (statusCode >= 400) { 1211 onError(response); 1212 return true; 1213 } 1214 } else if (evt instanceof TransactionTerminatedEvent) { 1215 // rfc3261#section-14.1: 1216 // if re-invite gets timed out, terminate the dialog; but 1217 // re-invite is not reliable, just let it go and pretend 1218 // nothing happened. 1219 onError(new SipException("timed out")); 1220 } 1221 return false; 1222 } 1223 1224 private boolean processReferRequest(RequestEvent event) 1225 throws SipException { 1226 try { 1227 ReferToHeader referto = (ReferToHeader) event.getRequest() 1228 .getHeader(ReferTo.NAME); 1229 Address address = referto.getAddress(); 1230 SipURI uri = (SipURI) address.getURI(); 1231 String replacesHeader = uri.getHeader(ReplacesHeader.NAME); 1232 String username = uri.getUser(); 1233 if (username == null) { 1234 mSipHelper.sendResponse(event, Response.BAD_REQUEST); 1235 return false; 1236 } 1237 // send notify accepted 1238 mSipHelper.sendResponse(event, Response.ACCEPTED); 1239 SipSessionImpl newSession = createNewSession(event, 1240 this.mProxy.getListener(), 1241 mSipHelper.getServerTransaction(event), 1242 SipSession.State.READY_TO_CALL); 1243 newSession.mReferSession = this; 1244 newSession.mReferredBy = (ReferredByHeader) event.getRequest() 1245 .getHeader(ReferredByHeader.NAME); 1246 newSession.mReplaces = replacesHeader; 1247 newSession.mPeerProfile = createPeerProfile(referto); 1248 newSession.mProxy.onCallTransferring(newSession, 1249 null); 1250 return true; 1251 } catch (IllegalArgumentException e) { 1252 throw new SipException("createPeerProfile()", e); 1253 } 1254 } 1255 1256 private boolean inCall(EventObject evt) throws SipException { 1257 // expect END_CALL cmd, BYE request, hold call (MakeCallCommand) 1258 // OK retransmission is handled in SipStack 1259 if (END_CALL == evt) { 1260 // rfc3261#section-15.1.1 1261 mState = SipSession.State.ENDING_CALL; 1262 mSipHelper.sendBye(mDialog); 1263 mProxy.onCallEnded(this); 1264 startSessionTimer(END_CALL_TIMER); 1265 return true; 1266 } else if (isRequestEvent(Request.INVITE, evt)) { 1267 // got Re-INVITE 1268 mState = SipSession.State.INCOMING_CALL; 1269 RequestEvent event = mInviteReceived = (RequestEvent) evt; 1270 mPeerSessionDescription = extractContent(event.getRequest()); 1271 mServerTransaction = null; 1272 mProxy.onRinging(this, mPeerProfile, mPeerSessionDescription); 1273 return true; 1274 } else if (isRequestEvent(Request.BYE, evt)) { 1275 mSipHelper.sendResponse((RequestEvent) evt, Response.OK); 1276 endCallNormally(); 1277 return true; 1278 } else if (isRequestEvent(Request.REFER, evt)) { 1279 return processReferRequest((RequestEvent) evt); 1280 } else if (evt instanceof MakeCallCommand) { 1281 // to change call 1282 mState = SipSession.State.OUTGOING_CALL; 1283 mClientTransaction = mSipHelper.sendReinvite(mDialog, 1284 ((MakeCallCommand) evt).getSessionDescription()); 1285 startSessionTimer(((MakeCallCommand) evt).getTimeout()); 1286 return true; 1287 } else if (evt instanceof ResponseEvent) { 1288 if (expectResponse(Request.NOTIFY, evt)) return true; 1289 } 1290 return false; 1291 } 1292 1293 private boolean endingCall(EventObject evt) throws SipException { 1294 if (expectResponse(Request.BYE, evt)) { 1295 ResponseEvent event = (ResponseEvent) evt; 1296 Response response = event.getResponse(); 1297 1298 int statusCode = response.getStatusCode(); 1299 switch (statusCode) { 1300 case Response.UNAUTHORIZED: 1301 case Response.PROXY_AUTHENTICATION_REQUIRED: 1302 if (handleAuthentication(event)) { 1303 return true; 1304 } else { 1305 // can't authenticate; pass through to end session 1306 } 1307 } 1308 cancelSessionTimer(); 1309 reset(); 1310 return true; 1311 } 1312 return false; 1313 } 1314 1315 // timeout in seconds 1316 private void startSessionTimer(int timeout) { 1317 if (timeout > 0) { 1318 mSessionTimer = new SessionTimer(); 1319 mSessionTimer.start(timeout); 1320 } 1321 } 1322 1323 private void cancelSessionTimer() { 1324 if (mSessionTimer != null) { 1325 mSessionTimer.cancel(); 1326 mSessionTimer = null; 1327 } 1328 } 1329 1330 private String createErrorMessage(Response response) { 1331 return String.format("%s (%d)", response.getReasonPhrase(), 1332 response.getStatusCode()); 1333 } 1334 1335 private void enableKeepAlive() { 1336 if (mKeepAliveSession != null) { 1337 mKeepAliveSession.stopKeepAliveProcess(); 1338 } else { 1339 mKeepAliveSession = duplicate(); 1340 } 1341 try { 1342 mKeepAliveSession.startKeepAliveProcess( 1343 INCALL_KEEPALIVE_INTERVAL, mPeerProfile, null); 1344 } catch (SipException e) { 1345 Log.w(TAG, "keepalive cannot be enabled; ignored", e); 1346 mKeepAliveSession.stopKeepAliveProcess(); 1347 } 1348 } 1349 1350 private void establishCall(boolean enableKeepAlive) { 1351 mState = SipSession.State.IN_CALL; 1352 cancelSessionTimer(); 1353 if (!mInCall && enableKeepAlive) enableKeepAlive(); 1354 mInCall = true; 1355 mProxy.onCallEstablished(this, mPeerSessionDescription); 1356 } 1357 1358 private void endCallNormally() { 1359 reset(); 1360 mProxy.onCallEnded(this); 1361 } 1362 1363 private void endCallOnError(int errorCode, String message) { 1364 reset(); 1365 mProxy.onError(this, errorCode, message); 1366 } 1367 1368 private void endCallOnBusy() { 1369 reset(); 1370 mProxy.onCallBusy(this); 1371 } 1372 1373 private void onError(int errorCode, String message) { 1374 cancelSessionTimer(); 1375 switch (mState) { 1376 case SipSession.State.REGISTERING: 1377 case SipSession.State.DEREGISTERING: 1378 onRegistrationFailed(errorCode, message); 1379 break; 1380 default: 1381 endCallOnError(errorCode, message); 1382 } 1383 } 1384 1385 1386 private void onError(Throwable exception) { 1387 exception = getRootCause(exception); 1388 onError(getErrorCode(exception), exception.toString()); 1389 } 1390 1391 private void onError(Response response) { 1392 int statusCode = response.getStatusCode(); 1393 if (!mInCall && (statusCode == Response.BUSY_HERE)) { 1394 endCallOnBusy(); 1395 } else { 1396 onError(getErrorCode(statusCode), createErrorMessage(response)); 1397 } 1398 } 1399 1400 private int getErrorCode(int responseStatusCode) { 1401 switch (responseStatusCode) { 1402 case Response.TEMPORARILY_UNAVAILABLE: 1403 case Response.FORBIDDEN: 1404 case Response.GONE: 1405 case Response.NOT_FOUND: 1406 case Response.NOT_ACCEPTABLE: 1407 case Response.NOT_ACCEPTABLE_HERE: 1408 return SipErrorCode.PEER_NOT_REACHABLE; 1409 1410 case Response.REQUEST_URI_TOO_LONG: 1411 case Response.ADDRESS_INCOMPLETE: 1412 case Response.AMBIGUOUS: 1413 return SipErrorCode.INVALID_REMOTE_URI; 1414 1415 case Response.REQUEST_TIMEOUT: 1416 return SipErrorCode.TIME_OUT; 1417 1418 default: 1419 if (responseStatusCode < 500) { 1420 return SipErrorCode.CLIENT_ERROR; 1421 } else { 1422 return SipErrorCode.SERVER_ERROR; 1423 } 1424 } 1425 } 1426 1427 private int getErrorCode(Throwable exception) { 1428 String message = exception.getMessage(); 1429 if (exception instanceof UnknownHostException) { 1430 return SipErrorCode.SERVER_UNREACHABLE; 1431 } else if (exception instanceof IOException) { 1432 return SipErrorCode.SOCKET_ERROR; 1433 } else { 1434 return SipErrorCode.CLIENT_ERROR; 1435 } 1436 } 1437 1438 private void onRegistrationDone(int duration) { 1439 reset(); 1440 mProxy.onRegistrationDone(this, duration); 1441 } 1442 1443 private void onRegistrationFailed(int errorCode, String message) { 1444 reset(); 1445 mProxy.onRegistrationFailed(this, errorCode, message); 1446 } 1447 1448 private void onRegistrationFailed(Throwable exception) { 1449 exception = getRootCause(exception); 1450 onRegistrationFailed(getErrorCode(exception), 1451 exception.toString()); 1452 } 1453 1454 private void onRegistrationFailed(Response response) { 1455 int statusCode = response.getStatusCode(); 1456 onRegistrationFailed(getErrorCode(statusCode), 1457 createErrorMessage(response)); 1458 } 1459 1460 // Notes: SipSessionListener will be replaced by the keepalive process 1461 // @param interval in seconds 1462 public void startKeepAliveProcess(int interval, 1463 KeepAliveProcessCallback callback) throws SipException { 1464 synchronized (SipSessionGroup.this) { 1465 startKeepAliveProcess(interval, mLocalProfile, callback); 1466 } 1467 } 1468 1469 // Notes: SipSessionListener will be replaced by the keepalive process 1470 // @param interval in seconds 1471 public void startKeepAliveProcess(int interval, SipProfile peerProfile, 1472 KeepAliveProcessCallback callback) throws SipException { 1473 synchronized (SipSessionGroup.this) { 1474 if (mKeepAliveProcess != null) { 1475 throw new SipException("Cannot create more than one " 1476 + "keepalive process in a SipSession"); 1477 } 1478 mPeerProfile = peerProfile; 1479 mKeepAliveProcess = new KeepAliveProcess(); 1480 mProxy.setListener(mKeepAliveProcess); 1481 mKeepAliveProcess.start(interval, callback); 1482 } 1483 } 1484 1485 public void stopKeepAliveProcess() { 1486 synchronized (SipSessionGroup.this) { 1487 if (mKeepAliveProcess != null) { 1488 mKeepAliveProcess.stop(); 1489 mKeepAliveProcess = null; 1490 } 1491 } 1492 } 1493 1494 class KeepAliveProcess extends SipSessionAdapter implements Runnable { 1495 private static final String TAG = "SipKeepAlive"; 1496 private boolean mRunning = false; 1497 private KeepAliveProcessCallback mCallback; 1498 1499 private boolean mPortChanged = false; 1500 private int mRPort = 0; 1501 private int mInterval; // just for debugging 1502 1503 // @param interval in seconds 1504 void start(int interval, KeepAliveProcessCallback callback) { 1505 if (mRunning) return; 1506 mRunning = true; 1507 mInterval = interval; 1508 mCallback = new KeepAliveProcessCallbackProxy(callback); 1509 mWakeupTimer.set(interval * 1000, this); 1510 if (DEBUG) { 1511 Log.d(TAG, "start keepalive:" 1512 + mLocalProfile.getUriString()); 1513 } 1514 1515 // No need to run the first time in a separate thread for now 1516 run(); 1517 } 1518 1519 // return true if the event is consumed 1520 boolean process(EventObject evt) throws SipException { 1521 if (mRunning && (mState == SipSession.State.PINGING)) { 1522 if (evt instanceof ResponseEvent) { 1523 if (parseOptionsResult(evt)) { 1524 if (mPortChanged) { 1525 resetExternalAddress(); 1526 stop(); 1527 } else { 1528 cancelSessionTimer(); 1529 removeSipSession(SipSessionImpl.this); 1530 } 1531 mCallback.onResponse(mPortChanged); 1532 return true; 1533 } 1534 } 1535 } 1536 return false; 1537 } 1538 1539 // SipSessionAdapter 1540 // To react to the session timeout event and network error. 1541 @Override 1542 public void onError(ISipSession session, int errorCode, String message) { 1543 stop(); 1544 mCallback.onError(errorCode, message); 1545 } 1546 1547 // SipWakeupTimer timeout handler 1548 // To send out keepalive message. 1549 @Override 1550 public void run() { 1551 synchronized (SipSessionGroup.this) { 1552 if (!mRunning) return; 1553 1554 if (DEBUG_PING) { 1555 String peerUri = (mPeerProfile == null) 1556 ? "null" 1557 : mPeerProfile.getUriString(); 1558 Log.d(TAG, "keepalive: " + mLocalProfile.getUriString() 1559 + " --> " + peerUri + ", interval=" + mInterval); 1560 } 1561 try { 1562 sendKeepAlive(); 1563 } catch (Throwable t) { 1564 if (DEBUG) { 1565 Log.w(TAG, "keepalive error: " 1566 + mLocalProfile.getUriString(), getRootCause(t)); 1567 } 1568 // It's possible that the keepalive process is being stopped 1569 // during session.sendKeepAlive() so need to check mRunning 1570 // again here. 1571 if (mRunning) SipSessionImpl.this.onError(t); 1572 } 1573 } 1574 } 1575 1576 void stop() { 1577 synchronized (SipSessionGroup.this) { 1578 if (DEBUG) { 1579 Log.d(TAG, "stop keepalive:" + mLocalProfile.getUriString() 1580 + ",RPort=" + mRPort); 1581 } 1582 mRunning = false; 1583 mWakeupTimer.cancel(this); 1584 reset(); 1585 } 1586 } 1587 1588 private void sendKeepAlive() throws SipException, InterruptedException { 1589 synchronized (SipSessionGroup.this) { 1590 mState = SipSession.State.PINGING; 1591 mClientTransaction = mSipHelper.sendOptions( 1592 mLocalProfile, mPeerProfile, generateTag()); 1593 mDialog = mClientTransaction.getDialog(); 1594 addSipSession(SipSessionImpl.this); 1595 1596 startSessionTimer(KEEPALIVE_TIMEOUT); 1597 // when timed out, onError() will be called with SipErrorCode.TIME_OUT 1598 } 1599 } 1600 1601 private boolean parseOptionsResult(EventObject evt) { 1602 if (expectResponse(Request.OPTIONS, evt)) { 1603 ResponseEvent event = (ResponseEvent) evt; 1604 int rPort = getRPortFromResponse(event.getResponse()); 1605 if (rPort != -1) { 1606 if (mRPort == 0) mRPort = rPort; 1607 if (mRPort != rPort) { 1608 mPortChanged = true; 1609 if (DEBUG) Log.d(TAG, String.format( 1610 "rport is changed: %d <> %d", mRPort, rPort)); 1611 mRPort = rPort; 1612 } else { 1613 if (DEBUG) Log.d(TAG, "rport is the same: " + rPort); 1614 } 1615 } else { 1616 if (DEBUG) Log.w(TAG, "peer did not respond rport"); 1617 } 1618 return true; 1619 } 1620 return false; 1621 } 1622 1623 private int getRPortFromResponse(Response response) { 1624 ViaHeader viaHeader = (ViaHeader)(response.getHeader( 1625 SIPHeaderNames.VIA)); 1626 return (viaHeader == null) ? -1 : viaHeader.getRPort(); 1627 } 1628 } 1629 } 1630 1631 /** 1632 * @return true if the event is a request event matching the specified 1633 * method; false otherwise 1634 */ 1635 private static boolean isRequestEvent(String method, EventObject event) { 1636 try { 1637 if (event instanceof RequestEvent) { 1638 RequestEvent requestEvent = (RequestEvent) event; 1639 return method.equals(requestEvent.getRequest().getMethod()); 1640 } 1641 } catch (Throwable e) { 1642 } 1643 return false; 1644 } 1645 1646 private static String getCseqMethod(Message message) { 1647 return ((CSeqHeader) message.getHeader(CSeqHeader.NAME)).getMethod(); 1648 } 1649 1650 /** 1651 * @return true if the event is a response event and the CSeqHeader method 1652 * match the given arguments; false otherwise 1653 */ 1654 private static boolean expectResponse( 1655 String expectedMethod, EventObject evt) { 1656 if (evt instanceof ResponseEvent) { 1657 ResponseEvent event = (ResponseEvent) evt; 1658 Response response = event.getResponse(); 1659 return expectedMethod.equalsIgnoreCase(getCseqMethod(response)); 1660 } 1661 return false; 1662 } 1663 1664 /** 1665 * @return true if the event is a response event and the response code and 1666 * CSeqHeader method match the given arguments; false otherwise 1667 */ 1668 private static boolean expectResponse( 1669 int responseCode, String expectedMethod, EventObject evt) { 1670 if (evt instanceof ResponseEvent) { 1671 ResponseEvent event = (ResponseEvent) evt; 1672 Response response = event.getResponse(); 1673 if (response.getStatusCode() == responseCode) { 1674 return expectedMethod.equalsIgnoreCase(getCseqMethod(response)); 1675 } 1676 } 1677 return false; 1678 } 1679 1680 private static SipProfile createPeerProfile(HeaderAddress header) 1681 throws SipException { 1682 try { 1683 Address address = header.getAddress(); 1684 SipURI uri = (SipURI) address.getURI(); 1685 String username = uri.getUser(); 1686 if (username == null) username = ANONYMOUS; 1687 int port = uri.getPort(); 1688 SipProfile.Builder builder = 1689 new SipProfile.Builder(username, uri.getHost()) 1690 .setDisplayName(address.getDisplayName()); 1691 if (port > 0) builder.setPort(port); 1692 return builder.build(); 1693 } catch (IllegalArgumentException e) { 1694 throw new SipException("createPeerProfile()", e); 1695 } catch (ParseException e) { 1696 throw new SipException("createPeerProfile()", e); 1697 } 1698 } 1699 1700 private static boolean isLoggable(SipSessionImpl s) { 1701 if (s != null) { 1702 switch (s.mState) { 1703 case SipSession.State.PINGING: 1704 return DEBUG_PING; 1705 } 1706 } 1707 return DEBUG; 1708 } 1709 1710 private static boolean isLoggable(EventObject evt) { 1711 return isLoggable(null, evt); 1712 } 1713 1714 private static boolean isLoggable(SipSessionImpl s, EventObject evt) { 1715 if (!isLoggable(s)) return false; 1716 if (evt == null) return false; 1717 1718 if (evt instanceof ResponseEvent) { 1719 Response response = ((ResponseEvent) evt).getResponse(); 1720 if (Request.OPTIONS.equals(response.getHeader(CSeqHeader.NAME))) { 1721 return DEBUG_PING; 1722 } 1723 return DEBUG; 1724 } else if (evt instanceof RequestEvent) { 1725 if (isRequestEvent(Request.OPTIONS, evt)) { 1726 return DEBUG_PING; 1727 } 1728 return DEBUG; 1729 } 1730 return false; 1731 } 1732 1733 private static String log(EventObject evt) { 1734 if (evt instanceof RequestEvent) { 1735 return ((RequestEvent) evt).getRequest().toString(); 1736 } else if (evt instanceof ResponseEvent) { 1737 return ((ResponseEvent) evt).getResponse().toString(); 1738 } else { 1739 return evt.toString(); 1740 } 1741 } 1742 1743 private class RegisterCommand extends EventObject { 1744 private int mDuration; 1745 1746 public RegisterCommand(int duration) { 1747 super(SipSessionGroup.this); 1748 mDuration = duration; 1749 } 1750 1751 public int getDuration() { 1752 return mDuration; 1753 } 1754 } 1755 1756 private class MakeCallCommand extends EventObject { 1757 private String mSessionDescription; 1758 private int mTimeout; // in seconds 1759 1760 public MakeCallCommand(SipProfile peerProfile, 1761 String sessionDescription) { 1762 this(peerProfile, sessionDescription, -1); 1763 } 1764 1765 public MakeCallCommand(SipProfile peerProfile, 1766 String sessionDescription, int timeout) { 1767 super(peerProfile); 1768 mSessionDescription = sessionDescription; 1769 mTimeout = timeout; 1770 } 1771 1772 public SipProfile getPeerProfile() { 1773 return (SipProfile) getSource(); 1774 } 1775 1776 public String getSessionDescription() { 1777 return mSessionDescription; 1778 } 1779 1780 public int getTimeout() { 1781 return mTimeout; 1782 } 1783 } 1784 1785 /** Class to help safely run KeepAliveProcessCallback in a different thread. */ 1786 static class KeepAliveProcessCallbackProxy implements KeepAliveProcessCallback { 1787 private KeepAliveProcessCallback mCallback; 1788 1789 KeepAliveProcessCallbackProxy(KeepAliveProcessCallback callback) { 1790 mCallback = callback; 1791 } 1792 1793 private void proxy(Runnable runnable) { 1794 // One thread for each calling back. 1795 // Note: Guarantee ordering if the issue becomes important. Currently, 1796 // the chance of handling two callback events at a time is none. 1797 new Thread(runnable, "SIP-KeepAliveProcessCallbackThread").start(); 1798 } 1799 1800 public void onResponse(final boolean portChanged) { 1801 if (mCallback == null) return; 1802 proxy(new Runnable() { 1803 public void run() { 1804 try { 1805 mCallback.onResponse(portChanged); 1806 } catch (Throwable t) { 1807 Log.w(TAG, "onResponse", t); 1808 } 1809 } 1810 }); 1811 } 1812 1813 public void onError(final int errorCode, final String description) { 1814 if (mCallback == null) return; 1815 proxy(new Runnable() { 1816 public void run() { 1817 try { 1818 mCallback.onError(errorCode, description); 1819 } catch (Throwable t) { 1820 Log.w(TAG, "onError", t); 1821 } 1822 } 1823 }); 1824 } 1825 } 1826 } 1827