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