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 android.net.sip; 18 19import android.content.Context; 20import android.media.AudioManager; 21import android.net.rtp.AudioCodec; 22import android.net.rtp.AudioGroup; 23import android.net.rtp.AudioStream; 24import android.net.rtp.RtpStream; 25import android.net.sip.SimpleSessionDescription.Media; 26import android.net.wifi.WifiManager; 27import android.os.Message; 28import android.telephony.Rlog; 29import android.text.TextUtils; 30import java.io.IOException; 31import java.net.InetAddress; 32import java.net.UnknownHostException; 33 34/** 35 * Handles an Internet audio call over SIP. You can instantiate this class with {@link SipManager}, 36 * using {@link SipManager#makeAudioCall makeAudioCall()} and {@link SipManager#takeAudioCall 37 * takeAudioCall()}. 38 * 39 * <p class="note"><strong>Note:</strong> Using this class require the 40 * {@link android.Manifest.permission#INTERNET} and 41 * {@link android.Manifest.permission#USE_SIP} permissions. In addition, {@link 42 * #startAudio} requires the 43 * {@link android.Manifest.permission#RECORD_AUDIO}, 44 * {@link android.Manifest.permission#ACCESS_WIFI_STATE}, and 45 * {@link android.Manifest.permission#WAKE_LOCK} permissions; and {@link #setSpeakerMode 46 * setSpeakerMode()} requires the 47 * {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p> 48 * 49 * <div class="special reference"> 50 * <h3>Developer Guides</h3> 51 * <p>For more information about using SIP, read the 52 * <a href="{@docRoot}guide/topics/network/sip.html">Session Initiation Protocol</a> 53 * developer guide.</p> 54 * </div> 55 */ 56public class SipAudioCall { 57 private static final String LOG_TAG = SipAudioCall.class.getSimpleName(); 58 private static final boolean DBG = true; 59 private static final boolean RELEASE_SOCKET = true; 60 private static final boolean DONT_RELEASE_SOCKET = false; 61 private static final int SESSION_TIMEOUT = 5; // in seconds 62 private static final int TRANSFER_TIMEOUT = 15; // in seconds 63 64 /** Listener for events relating to a SIP call, such as when a call is being 65 * recieved ("on ringing") or a call is outgoing ("on calling"). 66 * <p>Many of these events are also received by {@link SipSession.Listener}.</p> 67 */ 68 public static class Listener { 69 /** 70 * Called when the call object is ready to make another call. 71 * The default implementation calls {@link #onChanged}. 72 * 73 * @param call the call object that is ready to make another call 74 */ 75 public void onReadyToCall(SipAudioCall call) { 76 onChanged(call); 77 } 78 79 /** 80 * Called when a request is sent out to initiate a new call. 81 * The default implementation calls {@link #onChanged}. 82 * 83 * @param call the call object that carries out the audio call 84 */ 85 public void onCalling(SipAudioCall call) { 86 onChanged(call); 87 } 88 89 /** 90 * Called when a new call comes in. 91 * The default implementation calls {@link #onChanged}. 92 * 93 * @param call the call object that carries out the audio call 94 * @param caller the SIP profile of the caller 95 */ 96 public void onRinging(SipAudioCall call, SipProfile caller) { 97 onChanged(call); 98 } 99 100 /** 101 * Called when a RINGING response is received for the INVITE request 102 * sent. The default implementation calls {@link #onChanged}. 103 * 104 * @param call the call object that carries out the audio call 105 */ 106 public void onRingingBack(SipAudioCall call) { 107 onChanged(call); 108 } 109 110 /** 111 * Called when the session is established. 112 * The default implementation calls {@link #onChanged}. 113 * 114 * @param call the call object that carries out the audio call 115 */ 116 public void onCallEstablished(SipAudioCall call) { 117 onChanged(call); 118 } 119 120 /** 121 * Called when the session is terminated. 122 * The default implementation calls {@link #onChanged}. 123 * 124 * @param call the call object that carries out the audio call 125 */ 126 public void onCallEnded(SipAudioCall call) { 127 onChanged(call); 128 } 129 130 /** 131 * Called when the peer is busy during session initialization. 132 * The default implementation calls {@link #onChanged}. 133 * 134 * @param call the call object that carries out the audio call 135 */ 136 public void onCallBusy(SipAudioCall call) { 137 onChanged(call); 138 } 139 140 /** 141 * Called when the call is on hold. 142 * The default implementation calls {@link #onChanged}. 143 * 144 * @param call the call object that carries out the audio call 145 */ 146 public void onCallHeld(SipAudioCall call) { 147 onChanged(call); 148 } 149 150 /** 151 * Called when an error occurs. The default implementation is no op. 152 * 153 * @param call the call object that carries out the audio call 154 * @param errorCode error code of this error 155 * @param errorMessage error message 156 * @see SipErrorCode 157 */ 158 public void onError(SipAudioCall call, int errorCode, 159 String errorMessage) { 160 // no-op 161 } 162 163 /** 164 * Called when an event occurs and the corresponding callback is not 165 * overridden. The default implementation is no op. Error events are 166 * not re-directed to this callback and are handled in {@link #onError}. 167 */ 168 public void onChanged(SipAudioCall call) { 169 // no-op 170 } 171 } 172 173 private Context mContext; 174 private SipProfile mLocalProfile; 175 private SipAudioCall.Listener mListener; 176 private SipSession mSipSession; 177 private SipSession mTransferringSession; 178 179 private long mSessionId = System.currentTimeMillis(); 180 private String mPeerSd; 181 182 private AudioStream mAudioStream; 183 private AudioGroup mAudioGroup; 184 185 private boolean mInCall = false; 186 private boolean mMuted = false; 187 private boolean mHold = false; 188 189 private WifiManager mWm; 190 private WifiManager.WifiLock mWifiHighPerfLock; 191 192 private int mErrorCode = SipErrorCode.NO_ERROR; 193 private String mErrorMessage; 194 195 /** 196 * Creates a call object with the local SIP profile. 197 * @param context the context for accessing system services such as 198 * ringtone, audio, WIFI etc 199 */ 200 public SipAudioCall(Context context, SipProfile localProfile) { 201 mContext = context; 202 mLocalProfile = localProfile; 203 mWm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); 204 } 205 206 /** 207 * Sets the listener to listen to the audio call events. The method calls 208 * {@link #setListener setListener(listener, false)}. 209 * 210 * @param listener to listen to the audio call events of this object 211 * @see #setListener(Listener, boolean) 212 */ 213 public void setListener(SipAudioCall.Listener listener) { 214 setListener(listener, false); 215 } 216 217 /** 218 * Sets the listener to listen to the audio call events. A 219 * {@link SipAudioCall} can only hold one listener at a time. Subsequent 220 * calls to this method override the previous listener. 221 * 222 * @param listener to listen to the audio call events of this object 223 * @param callbackImmediately set to true if the caller wants to be called 224 * back immediately on the current state 225 */ 226 public void setListener(SipAudioCall.Listener listener, 227 boolean callbackImmediately) { 228 mListener = listener; 229 try { 230 if ((listener == null) || !callbackImmediately) { 231 // do nothing 232 } else if (mErrorCode != SipErrorCode.NO_ERROR) { 233 listener.onError(this, mErrorCode, mErrorMessage); 234 } else if (mInCall) { 235 if (mHold) { 236 listener.onCallHeld(this); 237 } else { 238 listener.onCallEstablished(this); 239 } 240 } else { 241 int state = getState(); 242 switch (state) { 243 case SipSession.State.READY_TO_CALL: 244 listener.onReadyToCall(this); 245 break; 246 case SipSession.State.INCOMING_CALL: 247 listener.onRinging(this, getPeerProfile()); 248 break; 249 case SipSession.State.OUTGOING_CALL: 250 listener.onCalling(this); 251 break; 252 case SipSession.State.OUTGOING_CALL_RING_BACK: 253 listener.onRingingBack(this); 254 break; 255 } 256 } 257 } catch (Throwable t) { 258 loge("setListener()", t); 259 } 260 } 261 262 /** 263 * Checks if the call is established. 264 * 265 * @return true if the call is established 266 */ 267 public boolean isInCall() { 268 synchronized (this) { 269 return mInCall; 270 } 271 } 272 273 /** 274 * Checks if the call is on hold. 275 * 276 * @return true if the call is on hold 277 */ 278 public boolean isOnHold() { 279 synchronized (this) { 280 return mHold; 281 } 282 } 283 284 /** 285 * Closes this object. This object is not usable after being closed. 286 */ 287 public void close() { 288 close(true); 289 } 290 291 private synchronized void close(boolean closeRtp) { 292 if (closeRtp) stopCall(RELEASE_SOCKET); 293 294 mInCall = false; 295 mHold = false; 296 mSessionId = System.currentTimeMillis(); 297 mErrorCode = SipErrorCode.NO_ERROR; 298 mErrorMessage = null; 299 300 if (mSipSession != null) { 301 mSipSession.setListener(null); 302 mSipSession = null; 303 } 304 } 305 306 /** 307 * Gets the local SIP profile. 308 * 309 * @return the local SIP profile 310 */ 311 public SipProfile getLocalProfile() { 312 synchronized (this) { 313 return mLocalProfile; 314 } 315 } 316 317 /** 318 * Gets the peer's SIP profile. 319 * 320 * @return the peer's SIP profile 321 */ 322 public SipProfile getPeerProfile() { 323 synchronized (this) { 324 return (mSipSession == null) ? null : mSipSession.getPeerProfile(); 325 } 326 } 327 328 /** 329 * Gets the state of the {@link SipSession} that carries this call. 330 * The value returned must be one of the states in {@link SipSession.State}. 331 * 332 * @return the session state 333 */ 334 public int getState() { 335 synchronized (this) { 336 if (mSipSession == null) return SipSession.State.READY_TO_CALL; 337 return mSipSession.getState(); 338 } 339 } 340 341 342 /** 343 * Gets the {@link SipSession} that carries this call. 344 * 345 * @return the session object that carries this call 346 * @hide 347 */ 348 public SipSession getSipSession() { 349 synchronized (this) { 350 return mSipSession; 351 } 352 } 353 354 private synchronized void transferToNewSession() { 355 if (mTransferringSession == null) return; 356 SipSession origin = mSipSession; 357 mSipSession = mTransferringSession; 358 mTransferringSession = null; 359 360 // stop the replaced call. 361 if (mAudioStream != null) { 362 mAudioStream.join(null); 363 } else { 364 try { 365 mAudioStream = new AudioStream(InetAddress.getByName( 366 getLocalIp())); 367 } catch (Throwable t) { 368 loge("transferToNewSession():", t); 369 } 370 } 371 if (origin != null) origin.endCall(); 372 startAudio(); 373 } 374 375 private SipSession.Listener createListener() { 376 return new SipSession.Listener() { 377 @Override 378 public void onCalling(SipSession session) { 379 if (DBG) log("onCalling: session=" + session); 380 Listener listener = mListener; 381 if (listener != null) { 382 try { 383 listener.onCalling(SipAudioCall.this); 384 } catch (Throwable t) { 385 loge("onCalling():", t); 386 } 387 } 388 } 389 390 @Override 391 public void onRingingBack(SipSession session) { 392 if (DBG) log("onRingingBackk: " + session); 393 Listener listener = mListener; 394 if (listener != null) { 395 try { 396 listener.onRingingBack(SipAudioCall.this); 397 } catch (Throwable t) { 398 loge("onRingingBack():", t); 399 } 400 } 401 } 402 403 @Override 404 public void onRinging(SipSession session, 405 SipProfile peerProfile, String sessionDescription) { 406 // this callback is triggered only for reinvite. 407 synchronized (SipAudioCall.this) { 408 if ((mSipSession == null) || !mInCall 409 || !session.getCallId().equals( 410 mSipSession.getCallId())) { 411 // should not happen 412 session.endCall(); 413 return; 414 } 415 416 // session changing request 417 try { 418 String answer = createAnswer(sessionDescription).encode(); 419 mSipSession.answerCall(answer, SESSION_TIMEOUT); 420 } catch (Throwable e) { 421 loge("onRinging():", e); 422 session.endCall(); 423 } 424 } 425 } 426 427 @Override 428 public void onCallEstablished(SipSession session, 429 String sessionDescription) { 430 mPeerSd = sessionDescription; 431 if (DBG) log("onCallEstablished(): " + mPeerSd); 432 433 // TODO: how to notify the UI that the remote party is changed 434 if ((mTransferringSession != null) 435 && (session == mTransferringSession)) { 436 transferToNewSession(); 437 return; 438 } 439 440 Listener listener = mListener; 441 if (listener != null) { 442 try { 443 if (mHold) { 444 listener.onCallHeld(SipAudioCall.this); 445 } else { 446 listener.onCallEstablished(SipAudioCall.this); 447 } 448 } catch (Throwable t) { 449 loge("onCallEstablished(): ", t); 450 } 451 } 452 } 453 454 @Override 455 public void onCallEnded(SipSession session) { 456 if (DBG) log("onCallEnded: " + session + " mSipSession:" + mSipSession); 457 // reset the trasnferring session if it is the one. 458 if (session == mTransferringSession) { 459 mTransferringSession = null; 460 return; 461 } 462 // or ignore the event if the original session is being 463 // transferred to the new one. 464 if ((mTransferringSession != null) || 465 (session != mSipSession)) return; 466 467 Listener listener = mListener; 468 if (listener != null) { 469 try { 470 listener.onCallEnded(SipAudioCall.this); 471 } catch (Throwable t) { 472 loge("onCallEnded(): ", t); 473 } 474 } 475 close(); 476 } 477 478 @Override 479 public void onCallBusy(SipSession session) { 480 if (DBG) log("onCallBusy: " + session); 481 Listener listener = mListener; 482 if (listener != null) { 483 try { 484 listener.onCallBusy(SipAudioCall.this); 485 } catch (Throwable t) { 486 loge("onCallBusy(): ", t); 487 } 488 } 489 close(false); 490 } 491 492 @Override 493 public void onCallChangeFailed(SipSession session, int errorCode, 494 String message) { 495 if (DBG) log("onCallChangedFailed: " + message); 496 mErrorCode = errorCode; 497 mErrorMessage = message; 498 Listener listener = mListener; 499 if (listener != null) { 500 try { 501 listener.onError(SipAudioCall.this, mErrorCode, 502 message); 503 } catch (Throwable t) { 504 loge("onCallBusy():", t); 505 } 506 } 507 } 508 509 @Override 510 public void onError(SipSession session, int errorCode, 511 String message) { 512 SipAudioCall.this.onError(errorCode, message); 513 } 514 515 @Override 516 public void onRegistering(SipSession session) { 517 // irrelevant 518 } 519 520 @Override 521 public void onRegistrationTimeout(SipSession session) { 522 // irrelevant 523 } 524 525 @Override 526 public void onRegistrationFailed(SipSession session, int errorCode, 527 String message) { 528 // irrelevant 529 } 530 531 @Override 532 public void onRegistrationDone(SipSession session, int duration) { 533 // irrelevant 534 } 535 536 @Override 537 public void onCallTransferring(SipSession newSession, 538 String sessionDescription) { 539 if (DBG) log("onCallTransferring: mSipSession=" 540 + mSipSession + " newSession=" + newSession); 541 mTransferringSession = newSession; 542 try { 543 if (sessionDescription == null) { 544 newSession.makeCall(newSession.getPeerProfile(), 545 createOffer().encode(), TRANSFER_TIMEOUT); 546 } else { 547 String answer = createAnswer(sessionDescription).encode(); 548 newSession.answerCall(answer, SESSION_TIMEOUT); 549 } 550 } catch (Throwable e) { 551 loge("onCallTransferring()", e); 552 newSession.endCall(); 553 } 554 } 555 }; 556 } 557 558 private void onError(int errorCode, String message) { 559 if (DBG) log("onError: " 560 + SipErrorCode.toString(errorCode) + ": " + message); 561 mErrorCode = errorCode; 562 mErrorMessage = message; 563 Listener listener = mListener; 564 if (listener != null) { 565 try { 566 listener.onError(this, errorCode, message); 567 } catch (Throwable t) { 568 loge("onError():", t); 569 } 570 } 571 synchronized (this) { 572 if ((errorCode == SipErrorCode.DATA_CONNECTION_LOST) 573 || !isInCall()) { 574 close(true); 575 } 576 } 577 } 578 579 /** 580 * Attaches an incoming call to this call object. 581 * 582 * @param session the session that receives the incoming call 583 * @param sessionDescription the session description of the incoming call 584 * @throws SipException if the SIP service fails to attach this object to 585 * the session or VOIP API is not supported by the device 586 * @see SipManager#isVoipSupported 587 */ 588 public void attachCall(SipSession session, String sessionDescription) 589 throws SipException { 590 if (!SipManager.isVoipSupported(mContext)) { 591 throw new SipException("VOIP API is not supported"); 592 } 593 594 synchronized (this) { 595 mSipSession = session; 596 mPeerSd = sessionDescription; 597 if (DBG) log("attachCall(): " + mPeerSd); 598 try { 599 session.setListener(createListener()); 600 } catch (Throwable e) { 601 loge("attachCall()", e); 602 throwSipException(e); 603 } 604 } 605 } 606 607 /** 608 * Initiates an audio call to the specified profile. The attempt will be 609 * timed out if the call is not established within {@code timeout} seconds 610 * and {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)} 611 * will be called. 612 * 613 * @param peerProfile the SIP profile to make the call to 614 * @param sipSession the {@link SipSession} for carrying out the call 615 * @param timeout the timeout value in seconds. Default value (defined by 616 * SIP protocol) is used if {@code timeout} is zero or negative. 617 * @see Listener#onError 618 * @throws SipException if the SIP service fails to create a session for the 619 * call or VOIP API is not supported by the device 620 * @see SipManager#isVoipSupported 621 */ 622 public void makeCall(SipProfile peerProfile, SipSession sipSession, 623 int timeout) throws SipException { 624 if (DBG) log("makeCall: " + peerProfile + " session=" + sipSession + " timeout=" + timeout); 625 if (!SipManager.isVoipSupported(mContext)) { 626 throw new SipException("VOIP API is not supported"); 627 } 628 629 synchronized (this) { 630 mSipSession = sipSession; 631 try { 632 mAudioStream = new AudioStream(InetAddress.getByName( 633 getLocalIp())); 634 sipSession.setListener(createListener()); 635 sipSession.makeCall(peerProfile, createOffer().encode(), 636 timeout); 637 } catch (IOException e) { 638 loge("makeCall:", e); 639 throw new SipException("makeCall()", e); 640 } 641 } 642 } 643 644 /** 645 * Ends a call. 646 * @throws SipException if the SIP service fails to end the call 647 */ 648 public void endCall() throws SipException { 649 if (DBG) log("endCall: mSipSession" + mSipSession); 650 synchronized (this) { 651 stopCall(RELEASE_SOCKET); 652 mInCall = false; 653 654 // perform the above local ops first and then network op 655 if (mSipSession != null) mSipSession.endCall(); 656 } 657 } 658 659 /** 660 * Puts a call on hold. When succeeds, {@link Listener#onCallHeld} is 661 * called. The attempt will be timed out if the call is not established 662 * within {@code timeout} seconds and 663 * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)} 664 * will be called. 665 * 666 * @param timeout the timeout value in seconds. Default value (defined by 667 * SIP protocol) is used if {@code timeout} is zero or negative. 668 * @see Listener#onError 669 * @throws SipException if the SIP service fails to hold the call 670 */ 671 public void holdCall(int timeout) throws SipException { 672 if (DBG) log("holdCall: mSipSession" + mSipSession + " timeout=" + timeout); 673 synchronized (this) { 674 if (mHold) return; 675 if (mSipSession == null) { 676 loge("holdCall:"); 677 throw new SipException("Not in a call to hold call"); 678 } 679 mSipSession.changeCall(createHoldOffer().encode(), timeout); 680 mHold = true; 681 setAudioGroupMode(); 682 } 683 } 684 685 /** 686 * Answers a call. The attempt will be timed out if the call is not 687 * established within {@code timeout} seconds and 688 * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)} 689 * will be called. 690 * 691 * @param timeout the timeout value in seconds. Default value (defined by 692 * SIP protocol) is used if {@code timeout} is zero or negative. 693 * @see Listener#onError 694 * @throws SipException if the SIP service fails to answer the call 695 */ 696 public void answerCall(int timeout) throws SipException { 697 if (DBG) log("answerCall: mSipSession" + mSipSession + " timeout=" + timeout); 698 synchronized (this) { 699 if (mSipSession == null) { 700 throw new SipException("No call to answer"); 701 } 702 try { 703 mAudioStream = new AudioStream(InetAddress.getByName( 704 getLocalIp())); 705 mSipSession.answerCall(createAnswer(mPeerSd).encode(), timeout); 706 } catch (IOException e) { 707 loge("answerCall:", e); 708 throw new SipException("answerCall()", e); 709 } 710 } 711 } 712 713 /** 714 * Continues a call that's on hold. When succeeds, 715 * {@link Listener#onCallEstablished} is called. The attempt will be timed 716 * out if the call is not established within {@code timeout} seconds and 717 * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)} 718 * will be called. 719 * 720 * @param timeout the timeout value in seconds. Default value (defined by 721 * SIP protocol) is used if {@code timeout} is zero or negative. 722 * @see Listener#onError 723 * @throws SipException if the SIP service fails to unhold the call 724 */ 725 public void continueCall(int timeout) throws SipException { 726 if (DBG) log("continueCall: mSipSession" + mSipSession + " timeout=" + timeout); 727 synchronized (this) { 728 if (!mHold) return; 729 mSipSession.changeCall(createContinueOffer().encode(), timeout); 730 mHold = false; 731 setAudioGroupMode(); 732 } 733 } 734 735 private SimpleSessionDescription createOffer() { 736 SimpleSessionDescription offer = 737 new SimpleSessionDescription(mSessionId, getLocalIp()); 738 AudioCodec[] codecs = AudioCodec.getCodecs(); 739 Media media = offer.newMedia( 740 "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP"); 741 for (AudioCodec codec : AudioCodec.getCodecs()) { 742 media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp); 743 } 744 media.setRtpPayload(127, "telephone-event/8000", "0-15"); 745 if (DBG) log("createOffer: offer=" + offer); 746 return offer; 747 } 748 749 private SimpleSessionDescription createAnswer(String offerSd) { 750 if (TextUtils.isEmpty(offerSd)) return createOffer(); 751 SimpleSessionDescription offer = 752 new SimpleSessionDescription(offerSd); 753 SimpleSessionDescription answer = 754 new SimpleSessionDescription(mSessionId, getLocalIp()); 755 AudioCodec codec = null; 756 for (Media media : offer.getMedia()) { 757 if ((codec == null) && (media.getPort() > 0) 758 && "audio".equals(media.getType()) 759 && "RTP/AVP".equals(media.getProtocol())) { 760 // Find the first audio codec we supported. 761 for (int type : media.getRtpPayloadTypes()) { 762 codec = AudioCodec.getCodec(type, media.getRtpmap(type), 763 media.getFmtp(type)); 764 if (codec != null) { 765 break; 766 } 767 } 768 if (codec != null) { 769 Media reply = answer.newMedia( 770 "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP"); 771 reply.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp); 772 773 // Check if DTMF is supported in the same media. 774 for (int type : media.getRtpPayloadTypes()) { 775 String rtpmap = media.getRtpmap(type); 776 if ((type != codec.type) && (rtpmap != null) 777 && rtpmap.startsWith("telephone-event")) { 778 reply.setRtpPayload( 779 type, rtpmap, media.getFmtp(type)); 780 } 781 } 782 783 // Handle recvonly and sendonly. 784 if (media.getAttribute("recvonly") != null) { 785 answer.setAttribute("sendonly", ""); 786 } else if(media.getAttribute("sendonly") != null) { 787 answer.setAttribute("recvonly", ""); 788 } else if(offer.getAttribute("recvonly") != null) { 789 answer.setAttribute("sendonly", ""); 790 } else if(offer.getAttribute("sendonly") != null) { 791 answer.setAttribute("recvonly", ""); 792 } 793 continue; 794 } 795 } 796 // Reject the media. 797 Media reply = answer.newMedia( 798 media.getType(), 0, 1, media.getProtocol()); 799 for (String format : media.getFormats()) { 800 reply.setFormat(format, null); 801 } 802 } 803 if (codec == null) { 804 loge("createAnswer: no suitable codes"); 805 throw new IllegalStateException("Reject SDP: no suitable codecs"); 806 } 807 if (DBG) log("createAnswer: answer=" + answer); 808 return answer; 809 } 810 811 private SimpleSessionDescription createHoldOffer() { 812 SimpleSessionDescription offer = createContinueOffer(); 813 offer.setAttribute("sendonly", ""); 814 if (DBG) log("createHoldOffer: offer=" + offer); 815 return offer; 816 } 817 818 private SimpleSessionDescription createContinueOffer() { 819 if (DBG) log("createContinueOffer"); 820 SimpleSessionDescription offer = 821 new SimpleSessionDescription(mSessionId, getLocalIp()); 822 Media media = offer.newMedia( 823 "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP"); 824 AudioCodec codec = mAudioStream.getCodec(); 825 media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp); 826 int dtmfType = mAudioStream.getDtmfType(); 827 if (dtmfType != -1) { 828 media.setRtpPayload(dtmfType, "telephone-event/8000", "0-15"); 829 } 830 return offer; 831 } 832 833 private void grabWifiHighPerfLock() { 834 if (mWifiHighPerfLock == null) { 835 if (DBG) log("grabWifiHighPerfLock:"); 836 mWifiHighPerfLock = ((WifiManager) 837 mContext.getSystemService(Context.WIFI_SERVICE)) 838 .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, LOG_TAG); 839 mWifiHighPerfLock.acquire(); 840 } 841 } 842 843 private void releaseWifiHighPerfLock() { 844 if (mWifiHighPerfLock != null) { 845 if (DBG) log("releaseWifiHighPerfLock:"); 846 mWifiHighPerfLock.release(); 847 mWifiHighPerfLock = null; 848 } 849 } 850 851 private boolean isWifiOn() { 852 return (mWm.getConnectionInfo().getBSSID() == null) ? false : true; 853 } 854 855 /** Toggles mute. */ 856 public void toggleMute() { 857 synchronized (this) { 858 mMuted = !mMuted; 859 setAudioGroupMode(); 860 } 861 } 862 863 /** 864 * Checks if the call is muted. 865 * 866 * @return true if the call is muted 867 */ 868 public boolean isMuted() { 869 synchronized (this) { 870 return mMuted; 871 } 872 } 873 874 /** 875 * Puts the device to speaker mode. 876 * <p class="note"><strong>Note:</strong> Requires the 877 * {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p> 878 * 879 * @param speakerMode set true to enable speaker mode; false to disable 880 */ 881 public void setSpeakerMode(boolean speakerMode) { 882 synchronized (this) { 883 ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)) 884 .setSpeakerphoneOn(speakerMode); 885 setAudioGroupMode(); 886 } 887 } 888 889 private boolean isSpeakerOn() { 890 return ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)) 891 .isSpeakerphoneOn(); 892 } 893 894 /** 895 * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>, 896 * event 0--9 maps to decimal 897 * value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event 898 * flash to 16. Currently, event flash is not supported. 899 * 900 * @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid 901 * inputs. 902 */ 903 public void sendDtmf(int code) { 904 sendDtmf(code, null); 905 } 906 907 /** 908 * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>, 909 * event 0--9 maps to decimal 910 * value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event 911 * flash to 16. Currently, event flash is not supported. 912 * 913 * @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid 914 * inputs. 915 * @param result the result message to send when done 916 */ 917 public void sendDtmf(int code, Message result) { 918 synchronized (this) { 919 AudioGroup audioGroup = getAudioGroup(); 920 if ((audioGroup != null) && (mSipSession != null) 921 && (SipSession.State.IN_CALL == getState())) { 922 if (DBG) log("sendDtmf: code=" + code + " result=" + result); 923 audioGroup.sendDtmf(code); 924 } 925 if (result != null) result.sendToTarget(); 926 } 927 } 928 929 /** 930 * Gets the {@link AudioStream} object used in this call. The object 931 * represents the RTP stream that carries the audio data to and from the 932 * peer. The object may not be created before the call is established. And 933 * it is undefined after the call ends or the {@link #close} method is 934 * called. 935 * 936 * @return the {@link AudioStream} object or null if the RTP stream has not 937 * yet been set up 938 * @hide 939 */ 940 public AudioStream getAudioStream() { 941 synchronized (this) { 942 return mAudioStream; 943 } 944 } 945 946 /** 947 * Gets the {@link AudioGroup} object which the {@link AudioStream} object 948 * joins. The group object may not exist before the call is established. 949 * Also, the {@code AudioStream} may change its group during a call (e.g., 950 * after the call is held/un-held). Finally, the {@code AudioGroup} object 951 * returned by this method is undefined after the call ends or the 952 * {@link #close} method is called. If a group object is set by 953 * {@link #setAudioGroup(AudioGroup)}, then this method returns that object. 954 * 955 * @return the {@link AudioGroup} object or null if the RTP stream has not 956 * yet been set up 957 * @see #getAudioStream 958 * @hide 959 */ 960 public AudioGroup getAudioGroup() { 961 synchronized (this) { 962 if (mAudioGroup != null) return mAudioGroup; 963 return ((mAudioStream == null) ? null : mAudioStream.getGroup()); 964 } 965 } 966 967 /** 968 * Sets the {@link AudioGroup} object which the {@link AudioStream} object 969 * joins. If {@code audioGroup} is null, then the {@code AudioGroup} object 970 * will be dynamically created when needed. Note that the mode of the 971 * {@code AudioGroup} is not changed according to the audio settings (i.e., 972 * hold, mute, speaker phone) of this object. This is mainly used to merge 973 * multiple {@code SipAudioCall} objects to form a conference call. The 974 * settings of the first object (that merges others) override others'. 975 * 976 * @see #getAudioStream 977 * @hide 978 */ 979 public void setAudioGroup(AudioGroup group) { 980 synchronized (this) { 981 if (DBG) log("setAudioGroup: group=" + group); 982 if ((mAudioStream != null) && (mAudioStream.getGroup() != null)) { 983 mAudioStream.join(group); 984 } 985 mAudioGroup = group; 986 } 987 } 988 989 /** 990 * Starts the audio for the established call. This method should be called 991 * after {@link Listener#onCallEstablished} is called. 992 * <p class="note"><strong>Note:</strong> Requires the 993 * {@link android.Manifest.permission#RECORD_AUDIO}, 994 * {@link android.Manifest.permission#ACCESS_WIFI_STATE} and 995 * {@link android.Manifest.permission#WAKE_LOCK} permissions.</p> 996 */ 997 public void startAudio() { 998 try { 999 startAudioInternal(); 1000 } catch (UnknownHostException e) { 1001 onError(SipErrorCode.PEER_NOT_REACHABLE, e.getMessage()); 1002 } catch (Throwable e) { 1003 onError(SipErrorCode.CLIENT_ERROR, e.getMessage()); 1004 } 1005 } 1006 1007 private synchronized void startAudioInternal() throws UnknownHostException { 1008 if (DBG) loge("startAudioInternal: mPeerSd=" + mPeerSd); 1009 if (mPeerSd == null) { 1010 throw new IllegalStateException("mPeerSd = null"); 1011 } 1012 1013 stopCall(DONT_RELEASE_SOCKET); 1014 mInCall = true; 1015 1016 // Run exact the same logic in createAnswer() to setup mAudioStream. 1017 SimpleSessionDescription offer = 1018 new SimpleSessionDescription(mPeerSd); 1019 AudioStream stream = mAudioStream; 1020 AudioCodec codec = null; 1021 for (Media media : offer.getMedia()) { 1022 if ((codec == null) && (media.getPort() > 0) 1023 && "audio".equals(media.getType()) 1024 && "RTP/AVP".equals(media.getProtocol())) { 1025 // Find the first audio codec we supported. 1026 for (int type : media.getRtpPayloadTypes()) { 1027 codec = AudioCodec.getCodec( 1028 type, media.getRtpmap(type), media.getFmtp(type)); 1029 if (codec != null) { 1030 break; 1031 } 1032 } 1033 1034 if (codec != null) { 1035 // Associate with the remote host. 1036 String address = media.getAddress(); 1037 if (address == null) { 1038 address = offer.getAddress(); 1039 } 1040 stream.associate(InetAddress.getByName(address), 1041 media.getPort()); 1042 1043 stream.setDtmfType(-1); 1044 stream.setCodec(codec); 1045 // Check if DTMF is supported in the same media. 1046 for (int type : media.getRtpPayloadTypes()) { 1047 String rtpmap = media.getRtpmap(type); 1048 if ((type != codec.type) && (rtpmap != null) 1049 && rtpmap.startsWith("telephone-event")) { 1050 stream.setDtmfType(type); 1051 } 1052 } 1053 1054 // Handle recvonly and sendonly. 1055 if (mHold) { 1056 stream.setMode(RtpStream.MODE_NORMAL); 1057 } else if (media.getAttribute("recvonly") != null) { 1058 stream.setMode(RtpStream.MODE_SEND_ONLY); 1059 } else if(media.getAttribute("sendonly") != null) { 1060 stream.setMode(RtpStream.MODE_RECEIVE_ONLY); 1061 } else if(offer.getAttribute("recvonly") != null) { 1062 stream.setMode(RtpStream.MODE_SEND_ONLY); 1063 } else if(offer.getAttribute("sendonly") != null) { 1064 stream.setMode(RtpStream.MODE_RECEIVE_ONLY); 1065 } else { 1066 stream.setMode(RtpStream.MODE_NORMAL); 1067 } 1068 break; 1069 } 1070 } 1071 } 1072 if (codec == null) { 1073 throw new IllegalStateException("Reject SDP: no suitable codecs"); 1074 } 1075 1076 if (isWifiOn()) grabWifiHighPerfLock(); 1077 1078 // AudioGroup logic: 1079 AudioGroup audioGroup = getAudioGroup(); 1080 if (mHold) { 1081 // don't create an AudioGroup here; doing so will fail if 1082 // there's another AudioGroup out there that's active 1083 } else { 1084 if (audioGroup == null) audioGroup = new AudioGroup(); 1085 stream.join(audioGroup); 1086 } 1087 setAudioGroupMode(); 1088 } 1089 1090 // set audio group mode based on current audio configuration 1091 private void setAudioGroupMode() { 1092 AudioGroup audioGroup = getAudioGroup(); 1093 if (DBG) log("setAudioGroupMode: audioGroup=" + audioGroup); 1094 if (audioGroup != null) { 1095 if (mHold) { 1096 audioGroup.setMode(AudioGroup.MODE_ON_HOLD); 1097 } else if (mMuted) { 1098 audioGroup.setMode(AudioGroup.MODE_MUTED); 1099 } else if (isSpeakerOn()) { 1100 audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION); 1101 } else { 1102 audioGroup.setMode(AudioGroup.MODE_NORMAL); 1103 } 1104 } 1105 } 1106 1107 private void stopCall(boolean releaseSocket) { 1108 if (DBG) log("stopCall: releaseSocket=" + releaseSocket); 1109 releaseWifiHighPerfLock(); 1110 if (mAudioStream != null) { 1111 mAudioStream.join(null); 1112 1113 if (releaseSocket) { 1114 mAudioStream.release(); 1115 mAudioStream = null; 1116 } 1117 } 1118 } 1119 1120 private String getLocalIp() { 1121 return mSipSession.getLocalIp(); 1122 } 1123 1124 private void throwSipException(Throwable throwable) throws SipException { 1125 if (throwable instanceof SipException) { 1126 throw (SipException) throwable; 1127 } else { 1128 throw new SipException("", throwable); 1129 } 1130 } 1131 1132 private void log(String s) { 1133 Rlog.d(LOG_TAG, s); 1134 } 1135 1136 private void loge(String s) { 1137 Rlog.e(LOG_TAG, s); 1138 } 1139 1140 private void loge(String s, Throwable t) { 1141 Rlog.e(LOG_TAG, s, t); 1142 } 1143} 1144