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