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