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