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