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