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