SipPhone.java revision 0825495a331bb44df395a0cdb79fab85e68db5d5
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.internal.telephony.sip; 18 19import android.content.Context; 20import android.media.AudioManager; 21import android.net.rtp.AudioGroup; 22import android.net.sip.SipAudioCall; 23import android.net.sip.SipErrorCode; 24import android.net.sip.SipException; 25import android.net.sip.SipManager; 26import android.net.sip.SipProfile; 27import android.net.sip.SipSession; 28import android.os.AsyncResult; 29import android.os.Message; 30import android.telephony.PhoneNumberUtils; 31import android.telephony.ServiceState; 32import android.text.TextUtils; 33import android.util.Log; 34 35import com.android.internal.telephony.Call; 36import com.android.internal.telephony.CallStateException; 37import com.android.internal.telephony.Connection; 38import com.android.internal.telephony.Phone; 39import com.android.internal.telephony.PhoneConstants; 40import com.android.internal.telephony.PhoneNotifier; 41 42import java.text.ParseException; 43import java.util.List; 44import java.util.regex.Pattern; 45 46/** 47 * {@hide} 48 */ 49public class SipPhone extends SipPhoneBase { 50 private static final String LOG_TAG = "SipPhone"; 51 private static final boolean DEBUG = true; 52 private static final int TIMEOUT_MAKE_CALL = 15; // in seconds 53 private static final int TIMEOUT_ANSWER_CALL = 8; // in seconds 54 private static final int TIMEOUT_HOLD_CALL = 15; // in seconds 55 56 // A call that is ringing or (call) waiting 57 private SipCall ringingCall = new SipCall(); 58 private SipCall foregroundCall = new SipCall(); 59 private SipCall backgroundCall = new SipCall(); 60 61 private SipManager mSipManager; 62 private SipProfile mProfile; 63 64 SipPhone (Context context, PhoneNotifier notifier, SipProfile profile) { 65 super(context, notifier); 66 67 if (DEBUG) Log.d(LOG_TAG, "new SipPhone: " + profile.getUriString()); 68 ringingCall = new SipCall(); 69 foregroundCall = new SipCall(); 70 backgroundCall = new SipCall(); 71 mProfile = profile; 72 mSipManager = SipManager.newInstance(context); 73 } 74 75 @Override 76 public boolean equals(Object o) { 77 if (o == this) return true; 78 if (!(o instanceof SipPhone)) return false; 79 SipPhone that = (SipPhone) o; 80 return mProfile.getUriString().equals(that.mProfile.getUriString()); 81 } 82 83 public String getPhoneName() { 84 return "SIP:" + getUriString(mProfile); 85 } 86 87 public String getSipUri() { 88 return mProfile.getUriString(); 89 } 90 91 public boolean equals(SipPhone phone) { 92 return getSipUri().equals(phone.getSipUri()); 93 } 94 95 public boolean canTake(Object incomingCall) { 96 synchronized (SipPhone.class) { 97 if (!(incomingCall instanceof SipAudioCall)) return false; 98 if (ringingCall.getState().isAlive()) return false; 99 100 // FIXME: is it true that we cannot take any incoming call if 101 // both foreground and background are active 102 if (foregroundCall.getState().isAlive() 103 && backgroundCall.getState().isAlive()) { 104 return false; 105 } 106 107 try { 108 SipAudioCall sipAudioCall = (SipAudioCall) incomingCall; 109 if (DEBUG) Log.d(LOG_TAG, "+++ taking call from: " 110 + sipAudioCall.getPeerProfile().getUriString()); 111 String localUri = sipAudioCall.getLocalProfile().getUriString(); 112 if (localUri.equals(mProfile.getUriString())) { 113 boolean makeCallWait = foregroundCall.getState().isAlive(); 114 ringingCall.initIncomingCall(sipAudioCall, makeCallWait); 115 if (sipAudioCall.getState() 116 != SipSession.State.INCOMING_CALL) { 117 // Peer cancelled the call! 118 if (DEBUG) Log.d(LOG_TAG, " call cancelled !!"); 119 ringingCall.reset(); 120 } 121 return true; 122 } 123 } catch (Exception e) { 124 // Peer may cancel the call at any time during the time we hook 125 // up ringingCall with sipAudioCall. Clean up ringingCall when 126 // that happens. 127 ringingCall.reset(); 128 } 129 return false; 130 } 131 } 132 133 public void acceptCall() throws CallStateException { 134 synchronized (SipPhone.class) { 135 if ((ringingCall.getState() == Call.State.INCOMING) || 136 (ringingCall.getState() == Call.State.WAITING)) { 137 if (DEBUG) Log.d(LOG_TAG, "acceptCall"); 138 // Always unmute when answering a new call 139 ringingCall.setMute(false); 140 ringingCall.acceptCall(); 141 } else { 142 throw new CallStateException("phone not ringing"); 143 } 144 } 145 } 146 147 public void rejectCall() throws CallStateException { 148 synchronized (SipPhone.class) { 149 if (ringingCall.getState().isRinging()) { 150 if (DEBUG) Log.d(LOG_TAG, "rejectCall"); 151 ringingCall.rejectCall(); 152 } else { 153 throw new CallStateException("phone not ringing"); 154 } 155 } 156 } 157 158 public Connection dial(String dialString) throws CallStateException { 159 synchronized (SipPhone.class) { 160 return dialInternal(dialString); 161 } 162 } 163 164 private Connection dialInternal(String dialString) 165 throws CallStateException { 166 clearDisconnected(); 167 168 if (!canDial()) { 169 throw new CallStateException("cannot dial in current state"); 170 } 171 if (foregroundCall.getState() == SipCall.State.ACTIVE) { 172 switchHoldingAndActive(); 173 } 174 if (foregroundCall.getState() != SipCall.State.IDLE) { 175 //we should have failed in !canDial() above before we get here 176 throw new CallStateException("cannot dial in current state"); 177 } 178 179 foregroundCall.setMute(false); 180 try { 181 Connection c = foregroundCall.dial(dialString); 182 return c; 183 } catch (SipException e) { 184 Log.e(LOG_TAG, "dial()", e); 185 throw new CallStateException("dial error: " + e); 186 } 187 } 188 189 public void switchHoldingAndActive() throws CallStateException { 190 if (DEBUG) Log.d(LOG_TAG, " ~~~~~~ switch fg and bg"); 191 synchronized (SipPhone.class) { 192 foregroundCall.switchWith(backgroundCall); 193 if (backgroundCall.getState().isAlive()) backgroundCall.hold(); 194 if (foregroundCall.getState().isAlive()) foregroundCall.unhold(); 195 } 196 } 197 198 public boolean canConference() { 199 return true; 200 } 201 202 public void conference() throws CallStateException { 203 synchronized (SipPhone.class) { 204 if ((foregroundCall.getState() != SipCall.State.ACTIVE) 205 || (foregroundCall.getState() != SipCall.State.ACTIVE)) { 206 throw new CallStateException("wrong state to merge calls: fg=" 207 + foregroundCall.getState() + ", bg=" 208 + backgroundCall.getState()); 209 } 210 foregroundCall.merge(backgroundCall); 211 } 212 } 213 214 public void conference(Call that) throws CallStateException { 215 synchronized (SipPhone.class) { 216 if (!(that instanceof SipCall)) { 217 throw new CallStateException("expect " + SipCall.class 218 + ", cannot merge with " + that.getClass()); 219 } 220 foregroundCall.merge((SipCall) that); 221 } 222 } 223 224 public boolean canTransfer() { 225 return false; 226 } 227 228 public void explicitCallTransfer() throws CallStateException { 229 //mCT.explicitCallTransfer(); 230 } 231 232 public void clearDisconnected() { 233 synchronized (SipPhone.class) { 234 ringingCall.clearDisconnected(); 235 foregroundCall.clearDisconnected(); 236 backgroundCall.clearDisconnected(); 237 238 updatePhoneState(); 239 notifyPreciseCallStateChanged(); 240 } 241 } 242 243 public void sendDtmf(char c) { 244 if (!PhoneNumberUtils.is12Key(c)) { 245 Log.e(LOG_TAG, 246 "sendDtmf called with invalid character '" + c + "'"); 247 } else if (foregroundCall.getState().isAlive()) { 248 synchronized (SipPhone.class) { 249 foregroundCall.sendDtmf(c); 250 } 251 } 252 } 253 254 public void startDtmf(char c) { 255 if (!PhoneNumberUtils.is12Key(c)) { 256 Log.e(LOG_TAG, 257 "startDtmf called with invalid character '" + c + "'"); 258 } else { 259 sendDtmf(c); 260 } 261 } 262 263 public void stopDtmf() { 264 // no op 265 } 266 267 public void sendBurstDtmf(String dtmfString) { 268 Log.e(LOG_TAG, "[SipPhone] sendBurstDtmf() is a CDMA method"); 269 } 270 271 public void getOutgoingCallerIdDisplay(Message onComplete) { 272 // FIXME: what to reply? 273 AsyncResult.forMessage(onComplete, null, null); 274 onComplete.sendToTarget(); 275 } 276 277 public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode, 278 Message onComplete) { 279 // FIXME: what's this for SIP? 280 AsyncResult.forMessage(onComplete, null, null); 281 onComplete.sendToTarget(); 282 } 283 284 public void getCallWaiting(Message onComplete) { 285 // FIXME: what to reply? 286 AsyncResult.forMessage(onComplete, null, null); 287 onComplete.sendToTarget(); 288 } 289 290 public void setCallWaiting(boolean enable, Message onComplete) { 291 // FIXME: what to reply? 292 Log.e(LOG_TAG, "call waiting not supported"); 293 } 294 295 @Override 296 public void setEchoSuppressionEnabled(boolean enabled) { 297 // TODO: Remove the enabled argument. We should check the speakerphone 298 // state with AudioManager instead of keeping a state here so the 299 // method with a state argument is redundant. Also rename the method 300 // to something like onSpeaerphoneStateChanged(). Echo suppression may 301 // not be available on every device. 302 synchronized (SipPhone.class) { 303 foregroundCall.setAudioGroupMode(); 304 } 305 } 306 307 public void setMute(boolean muted) { 308 synchronized (SipPhone.class) { 309 foregroundCall.setMute(muted); 310 } 311 } 312 313 public boolean getMute() { 314 return (foregroundCall.getState().isAlive() 315 ? foregroundCall.getMute() 316 : backgroundCall.getMute()); 317 } 318 319 public Call getForegroundCall() { 320 return foregroundCall; 321 } 322 323 public Call getBackgroundCall() { 324 return backgroundCall; 325 } 326 327 public Call getRingingCall() { 328 return ringingCall; 329 } 330 331 public ServiceState getServiceState() { 332 // FIXME: we may need to provide this when data connectivity is lost 333 // or when server is down 334 return super.getServiceState(); 335 } 336 337 private String getUriString(SipProfile p) { 338 // SipProfile.getUriString() may contain "SIP:" and port 339 return p.getUserName() + "@" + getSipDomain(p); 340 } 341 342 private String getSipDomain(SipProfile p) { 343 String domain = p.getSipDomain(); 344 // TODO: move this to SipProfile 345 if (domain.endsWith(":5060")) { 346 return domain.substring(0, domain.length() - 5); 347 } else { 348 return domain; 349 } 350 } 351 352 private class SipCall extends SipCallBase { 353 void reset() { 354 connections.clear(); 355 setState(Call.State.IDLE); 356 } 357 358 void switchWith(SipCall that) { 359 synchronized (SipPhone.class) { 360 SipCall tmp = new SipCall(); 361 tmp.takeOver(this); 362 this.takeOver(that); 363 that.takeOver(tmp); 364 } 365 } 366 367 private void takeOver(SipCall that) { 368 connections = that.connections; 369 state = that.state; 370 for (Connection c : connections) { 371 ((SipConnection) c).changeOwner(this); 372 } 373 } 374 375 @Override 376 public Phone getPhone() { 377 return SipPhone.this; 378 } 379 380 @Override 381 public List<Connection> getConnections() { 382 synchronized (SipPhone.class) { 383 // FIXME should return Collections.unmodifiableList(); 384 return connections; 385 } 386 } 387 388 Connection dial(String originalNumber) throws SipException { 389 String calleeSipUri = originalNumber; 390 if (!calleeSipUri.contains("@")) { 391 String replaceStr = Pattern.quote(mProfile.getUserName() + "@"); 392 calleeSipUri = mProfile.getUriString().replaceFirst(replaceStr, 393 calleeSipUri + "@"); 394 } 395 try { 396 SipProfile callee = 397 new SipProfile.Builder(calleeSipUri).build(); 398 SipConnection c = new SipConnection(this, callee, 399 originalNumber); 400 c.dial(); 401 connections.add(c); 402 setState(Call.State.DIALING); 403 return c; 404 } catch (ParseException e) { 405 throw new SipException("dial", e); 406 } 407 } 408 409 @Override 410 public void hangup() throws CallStateException { 411 synchronized (SipPhone.class) { 412 if (state.isAlive()) { 413 if (DEBUG) Log.d(LOG_TAG, "hang up call: " + getState() 414 + ": " + this + " on phone " + getPhone()); 415 setState(State.DISCONNECTING); 416 CallStateException excp = null; 417 for (Connection c : connections) { 418 try { 419 c.hangup(); 420 } catch (CallStateException e) { 421 excp = e; 422 } 423 } 424 if (excp != null) throw excp; 425 } else { 426 if (DEBUG) Log.d(LOG_TAG, "hang up dead call: " + getState() 427 + ": " + this + " on phone " + getPhone()); 428 } 429 } 430 } 431 432 void initIncomingCall(SipAudioCall sipAudioCall, boolean makeCallWait) { 433 SipProfile callee = sipAudioCall.getPeerProfile(); 434 SipConnection c = new SipConnection(this, callee); 435 connections.add(c); 436 437 Call.State newState = makeCallWait ? State.WAITING : State.INCOMING; 438 c.initIncomingCall(sipAudioCall, newState); 439 440 setState(newState); 441 notifyNewRingingConnectionP(c); 442 } 443 444 void rejectCall() throws CallStateException { 445 hangup(); 446 } 447 448 void acceptCall() throws CallStateException { 449 if (this != ringingCall) { 450 throw new CallStateException("acceptCall() in a non-ringing call"); 451 } 452 if (connections.size() != 1) { 453 throw new CallStateException("acceptCall() in a conf call"); 454 } 455 ((SipConnection) connections.get(0)).acceptCall(); 456 } 457 458 private boolean isSpeakerOn() { 459 return ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)) 460 .isSpeakerphoneOn(); 461 } 462 463 void setAudioGroupMode() { 464 AudioGroup audioGroup = getAudioGroup(); 465 if (audioGroup == null) return; 466 int mode = audioGroup.getMode(); 467 if (state == State.HOLDING) { 468 audioGroup.setMode(AudioGroup.MODE_ON_HOLD); 469 } else if (getMute()) { 470 audioGroup.setMode(AudioGroup.MODE_MUTED); 471 } else if (isSpeakerOn()) { 472 audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION); 473 } else { 474 audioGroup.setMode(AudioGroup.MODE_NORMAL); 475 } 476 if (DEBUG) Log.d(LOG_TAG, String.format( 477 "audioGroup mode change: %d --> %d", mode, 478 audioGroup.getMode())); 479 } 480 481 void hold() throws CallStateException { 482 setState(State.HOLDING); 483 for (Connection c : connections) ((SipConnection) c).hold(); 484 setAudioGroupMode(); 485 } 486 487 void unhold() throws CallStateException { 488 setState(State.ACTIVE); 489 AudioGroup audioGroup = new AudioGroup(); 490 for (Connection c : connections) { 491 ((SipConnection) c).unhold(audioGroup); 492 } 493 setAudioGroupMode(); 494 } 495 496 void setMute(boolean muted) { 497 for (Connection c : connections) { 498 ((SipConnection) c).setMute(muted); 499 } 500 } 501 502 boolean getMute() { 503 return connections.isEmpty() 504 ? false 505 : ((SipConnection) connections.get(0)).getMute(); 506 } 507 508 void merge(SipCall that) throws CallStateException { 509 AudioGroup audioGroup = getAudioGroup(); 510 511 // copy to an array to avoid concurrent modification as connections 512 // in that.connections will be removed in add(SipConnection). 513 Connection[] cc = that.connections.toArray( 514 new Connection[that.connections.size()]); 515 for (Connection c : cc) { 516 SipConnection conn = (SipConnection) c; 517 add(conn); 518 if (conn.getState() == Call.State.HOLDING) { 519 conn.unhold(audioGroup); 520 } 521 } 522 that.setState(Call.State.IDLE); 523 } 524 525 private void add(SipConnection conn) { 526 SipCall call = conn.getCall(); 527 if (call == this) return; 528 if (call != null) call.connections.remove(conn); 529 530 connections.add(conn); 531 conn.changeOwner(this); 532 } 533 534 void sendDtmf(char c) { 535 AudioGroup audioGroup = getAudioGroup(); 536 if (audioGroup == null) return; 537 audioGroup.sendDtmf(convertDtmf(c)); 538 } 539 540 private int convertDtmf(char c) { 541 int code = c - '0'; 542 if ((code < 0) || (code > 9)) { 543 switch (c) { 544 case '*': return 10; 545 case '#': return 11; 546 case 'A': return 12; 547 case 'B': return 13; 548 case 'C': return 14; 549 case 'D': return 15; 550 default: 551 throw new IllegalArgumentException( 552 "invalid DTMF char: " + (int) c); 553 } 554 } 555 return code; 556 } 557 558 @Override 559 protected void setState(State newState) { 560 if (state != newState) { 561 if (DEBUG) Log.v(LOG_TAG, "+***+ call state changed: " + state 562 + " --> " + newState + ": " + this + ": on phone " 563 + getPhone() + " " + connections.size()); 564 565 if (newState == Call.State.ALERTING) { 566 state = newState; // need in ALERTING to enable ringback 567 SipPhone.this.startRingbackTone(); 568 } else if (state == Call.State.ALERTING) { 569 SipPhone.this.stopRingbackTone(); 570 } 571 state = newState; 572 updatePhoneState(); 573 notifyPreciseCallStateChanged(); 574 } 575 } 576 577 void onConnectionStateChanged(SipConnection conn) { 578 // this can be called back when a conf call is formed 579 if (state != State.ACTIVE) { 580 setState(conn.getState()); 581 } 582 } 583 584 void onConnectionEnded(SipConnection conn) { 585 // set state to DISCONNECTED only when all conns are disconnected 586 if (state != State.DISCONNECTED) { 587 boolean allConnectionsDisconnected = true; 588 if (DEBUG) Log.d(LOG_TAG, "---check connections: " 589 + connections.size()); 590 for (Connection c : connections) { 591 if (DEBUG) Log.d(LOG_TAG, " state=" + c.getState() + ": " 592 + c); 593 if (c.getState() != State.DISCONNECTED) { 594 allConnectionsDisconnected = false; 595 break; 596 } 597 } 598 if (allConnectionsDisconnected) setState(State.DISCONNECTED); 599 } 600 notifyDisconnectP(conn); 601 } 602 603 private AudioGroup getAudioGroup() { 604 if (connections.isEmpty()) return null; 605 return ((SipConnection) connections.get(0)).getAudioGroup(); 606 } 607 } 608 609 private class SipConnection extends SipConnectionBase { 610 private SipCall mOwner; 611 private SipAudioCall mSipAudioCall; 612 private Call.State mState = Call.State.IDLE; 613 private SipProfile mPeer; 614 private String mOriginalNumber; // may be a PSTN number 615 private boolean mIncoming = false; 616 617 private SipAudioCallAdapter mAdapter = new SipAudioCallAdapter() { 618 @Override 619 protected void onCallEnded(DisconnectCause cause) { 620 if (getDisconnectCause() != DisconnectCause.LOCAL) { 621 setDisconnectCause(cause); 622 } 623 synchronized (SipPhone.class) { 624 setState(Call.State.DISCONNECTED); 625 SipAudioCall sipAudioCall = mSipAudioCall; 626 mSipAudioCall = null; 627 String sessionState = (sipAudioCall == null) 628 ? "" 629 : (sipAudioCall.getState() + ", "); 630 if (DEBUG) Log.d(LOG_TAG, "--- connection ended: " 631 + mPeer.getUriString() + ": " + sessionState 632 + "cause: " + getDisconnectCause() + ", on phone " 633 + getPhone()); 634 if (sipAudioCall != null) { 635 sipAudioCall.setListener(null); 636 sipAudioCall.close(); 637 } 638 mOwner.onConnectionEnded(SipConnection.this); 639 } 640 } 641 642 @Override 643 public void onCallEstablished(SipAudioCall call) { 644 onChanged(call); 645 if (mState == Call.State.ACTIVE) call.startAudio(); 646 } 647 648 @Override 649 public void onCallHeld(SipAudioCall call) { 650 onChanged(call); 651 if (mState == Call.State.HOLDING) call.startAudio(); 652 } 653 654 @Override 655 public void onChanged(SipAudioCall call) { 656 synchronized (SipPhone.class) { 657 Call.State newState = getCallStateFrom(call); 658 if (mState == newState) return; 659 if (newState == Call.State.INCOMING) { 660 setState(mOwner.getState()); // INCOMING or WAITING 661 } else { 662 if (mOwner == ringingCall) { 663 if (ringingCall.getState() == Call.State.WAITING) { 664 try { 665 switchHoldingAndActive(); 666 } catch (CallStateException e) { 667 // disconnect the call. 668 onCallEnded(DisconnectCause.LOCAL); 669 return; 670 } 671 } 672 foregroundCall.switchWith(ringingCall); 673 } 674 setState(newState); 675 } 676 mOwner.onConnectionStateChanged(SipConnection.this); 677 if (DEBUG) Log.v(LOG_TAG, "+***+ connection state changed: " 678 + mPeer.getUriString() + ": " + mState 679 + " on phone " + getPhone()); 680 } 681 } 682 683 @Override 684 protected void onError(DisconnectCause cause) { 685 if (DEBUG) Log.d(LOG_TAG, "SIP error: " + cause); 686 onCallEnded(cause); 687 } 688 }; 689 690 public SipConnection(SipCall owner, SipProfile callee, 691 String originalNumber) { 692 super(originalNumber); 693 mOwner = owner; 694 mPeer = callee; 695 mOriginalNumber = originalNumber; 696 } 697 698 public SipConnection(SipCall owner, SipProfile callee) { 699 this(owner, callee, getUriString(callee)); 700 } 701 702 @Override 703 public String getCnapName() { 704 String displayName = mPeer.getDisplayName(); 705 return TextUtils.isEmpty(displayName) ? null 706 : displayName; 707 } 708 709 @Override 710 public int getNumberPresentation() { 711 return PhoneConstants.PRESENTATION_ALLOWED; 712 } 713 714 void initIncomingCall(SipAudioCall sipAudioCall, Call.State newState) { 715 setState(newState); 716 mSipAudioCall = sipAudioCall; 717 sipAudioCall.setListener(mAdapter); // call back to set state 718 mIncoming = true; 719 } 720 721 void acceptCall() throws CallStateException { 722 try { 723 mSipAudioCall.answerCall(TIMEOUT_ANSWER_CALL); 724 } catch (SipException e) { 725 throw new CallStateException("acceptCall(): " + e); 726 } 727 } 728 729 void changeOwner(SipCall owner) { 730 mOwner = owner; 731 } 732 733 AudioGroup getAudioGroup() { 734 if (mSipAudioCall == null) return null; 735 return mSipAudioCall.getAudioGroup(); 736 } 737 738 void dial() throws SipException { 739 setState(Call.State.DIALING); 740 mSipAudioCall = mSipManager.makeAudioCall(mProfile, mPeer, null, 741 TIMEOUT_MAKE_CALL); 742 mSipAudioCall.setListener(mAdapter); 743 } 744 745 void hold() throws CallStateException { 746 setState(Call.State.HOLDING); 747 try { 748 mSipAudioCall.holdCall(TIMEOUT_HOLD_CALL); 749 } catch (SipException e) { 750 throw new CallStateException("hold(): " + e); 751 } 752 } 753 754 void unhold(AudioGroup audioGroup) throws CallStateException { 755 mSipAudioCall.setAudioGroup(audioGroup); 756 setState(Call.State.ACTIVE); 757 try { 758 mSipAudioCall.continueCall(TIMEOUT_HOLD_CALL); 759 } catch (SipException e) { 760 throw new CallStateException("unhold(): " + e); 761 } 762 } 763 764 void setMute(boolean muted) { 765 if ((mSipAudioCall != null) && (muted != mSipAudioCall.isMuted())) { 766 mSipAudioCall.toggleMute(); 767 } 768 } 769 770 boolean getMute() { 771 return (mSipAudioCall == null) ? false 772 : mSipAudioCall.isMuted(); 773 } 774 775 @Override 776 protected void setState(Call.State state) { 777 if (state == mState) return; 778 super.setState(state); 779 mState = state; 780 } 781 782 @Override 783 public Call.State getState() { 784 return mState; 785 } 786 787 @Override 788 public boolean isIncoming() { 789 return mIncoming; 790 } 791 792 @Override 793 public String getAddress() { 794 // Phone app uses this to query caller ID. Return the original dial 795 // number (which may be a PSTN number) instead of the peer's SIP 796 // URI. 797 return mOriginalNumber; 798 } 799 800 @Override 801 public SipCall getCall() { 802 return mOwner; 803 } 804 805 @Override 806 protected Phone getPhone() { 807 return mOwner.getPhone(); 808 } 809 810 @Override 811 public void hangup() throws CallStateException { 812 synchronized (SipPhone.class) { 813 if (DEBUG) Log.d(LOG_TAG, "hangup conn: " + mPeer.getUriString() 814 + ": " + mState + ": on phone " 815 + getPhone().getPhoneName()); 816 if (!mState.isAlive()) return; 817 try { 818 SipAudioCall sipAudioCall = mSipAudioCall; 819 if (sipAudioCall != null) { 820 sipAudioCall.setListener(null); 821 sipAudioCall.endCall(); 822 } 823 } catch (SipException e) { 824 throw new CallStateException("hangup(): " + e); 825 } finally { 826 mAdapter.onCallEnded(((mState == Call.State.INCOMING) 827 || (mState == Call.State.WAITING)) 828 ? DisconnectCause.INCOMING_REJECTED 829 : DisconnectCause.LOCAL); 830 } 831 } 832 } 833 834 @Override 835 public void separate() throws CallStateException { 836 synchronized (SipPhone.class) { 837 SipCall call = (getPhone() == SipPhone.this) 838 ? (SipCall) SipPhone.this.getBackgroundCall() 839 : (SipCall) SipPhone.this.getForegroundCall(); 840 if (call.getState() != Call.State.IDLE) { 841 throw new CallStateException( 842 "cannot put conn back to a call in non-idle state: " 843 + call.getState()); 844 } 845 if (DEBUG) Log.d(LOG_TAG, "separate conn: " 846 + mPeer.getUriString() + " from " + mOwner + " back to " 847 + call); 848 849 // separate the AudioGroup and connection from the original call 850 Phone originalPhone = getPhone(); 851 AudioGroup audioGroup = call.getAudioGroup(); // may be null 852 call.add(this); 853 mSipAudioCall.setAudioGroup(audioGroup); 854 855 // put the original call to bg; and the separated call becomes 856 // fg if it was in bg 857 originalPhone.switchHoldingAndActive(); 858 859 // start audio and notify the phone app of the state change 860 call = (SipCall) SipPhone.this.getForegroundCall(); 861 mSipAudioCall.startAudio(); 862 call.onConnectionStateChanged(this); 863 } 864 } 865 866 } 867 868 private static Call.State getCallStateFrom(SipAudioCall sipAudioCall) { 869 if (sipAudioCall.isOnHold()) return Call.State.HOLDING; 870 int sessionState = sipAudioCall.getState(); 871 switch (sessionState) { 872 case SipSession.State.READY_TO_CALL: return Call.State.IDLE; 873 case SipSession.State.INCOMING_CALL: 874 case SipSession.State.INCOMING_CALL_ANSWERING: return Call.State.INCOMING; 875 case SipSession.State.OUTGOING_CALL: return Call.State.DIALING; 876 case SipSession.State.OUTGOING_CALL_RING_BACK: return Call.State.ALERTING; 877 case SipSession.State.OUTGOING_CALL_CANCELING: return Call.State.DISCONNECTING; 878 case SipSession.State.IN_CALL: return Call.State.ACTIVE; 879 default: 880 Log.w(LOG_TAG, "illegal connection state: " + sessionState); 881 return Call.State.DISCONNECTED; 882 } 883 } 884 885 private abstract class SipAudioCallAdapter extends SipAudioCall.Listener { 886 protected abstract void onCallEnded(Connection.DisconnectCause cause); 887 protected abstract void onError(Connection.DisconnectCause cause); 888 889 @Override 890 public void onCallEnded(SipAudioCall call) { 891 onCallEnded(call.isInCall() 892 ? Connection.DisconnectCause.NORMAL 893 : Connection.DisconnectCause.INCOMING_MISSED); 894 } 895 896 @Override 897 public void onCallBusy(SipAudioCall call) { 898 onCallEnded(Connection.DisconnectCause.BUSY); 899 } 900 901 @Override 902 public void onError(SipAudioCall call, int errorCode, 903 String errorMessage) { 904 switch (errorCode) { 905 case SipErrorCode.SERVER_UNREACHABLE: 906 onError(Connection.DisconnectCause.SERVER_UNREACHABLE); 907 break; 908 case SipErrorCode.PEER_NOT_REACHABLE: 909 onError(Connection.DisconnectCause.NUMBER_UNREACHABLE); 910 break; 911 case SipErrorCode.INVALID_REMOTE_URI: 912 onError(Connection.DisconnectCause.INVALID_NUMBER); 913 break; 914 case SipErrorCode.TIME_OUT: 915 case SipErrorCode.TRANSACTION_TERMINTED: 916 onError(Connection.DisconnectCause.TIMED_OUT); 917 break; 918 case SipErrorCode.DATA_CONNECTION_LOST: 919 onError(Connection.DisconnectCause.LOST_SIGNAL); 920 break; 921 case SipErrorCode.INVALID_CREDENTIALS: 922 onError(Connection.DisconnectCause.INVALID_CREDENTIALS); 923 break; 924 case SipErrorCode.CROSS_DOMAIN_AUTHENTICATION: 925 onError(Connection.DisconnectCause.OUT_OF_NETWORK); 926 break; 927 case SipErrorCode.SERVER_ERROR: 928 onError(Connection.DisconnectCause.SERVER_ERROR); 929 break; 930 case SipErrorCode.SOCKET_ERROR: 931 case SipErrorCode.CLIENT_ERROR: 932 default: 933 Log.w(LOG_TAG, "error: " + SipErrorCode.toString(errorCode) 934 + ": " + errorMessage); 935 onError(Connection.DisconnectCause.ERROR_UNSPECIFIED); 936 } 937 } 938 } 939} 940