1 /* 2 * Copyright (c) 2013 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.ims; 18 19 import com.android.internal.R; 20 21 import java.util.ArrayList; 22 import java.util.Iterator; 23 import java.util.List; 24 import java.util.Map.Entry; 25 import java.util.Set; 26 27 import android.content.Context; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.os.Message; 31 import android.os.Parcel; 32 import android.telecom.ConferenceParticipant; 33 import android.telecom.Connection; 34 import android.telephony.Rlog; 35 import java.util.Objects; 36 import java.util.concurrent.atomic.AtomicInteger; 37 38 import android.telephony.ServiceState; 39 import android.telephony.ims.ImsCallProfile; 40 import android.telephony.ims.ImsConferenceState; 41 import android.telephony.ims.ImsReasonInfo; 42 import android.telephony.ims.ImsStreamMediaProfile; 43 import android.telephony.ims.ImsSuppServiceNotification; 44 import android.util.Log; 45 46 import com.android.ims.internal.ICall; 47 import android.telephony.ims.ImsCallSession; 48 import com.android.ims.internal.ImsStreamMediaSession; 49 import com.android.internal.annotations.VisibleForTesting; 50 51 /** 52 * Handles an IMS voice / video call over LTE. You can instantiate this class with 53 * {@link ImsManager}. 54 * 55 * @hide 56 */ 57 public class ImsCall implements ICall { 58 // Mode of USSD message 59 public static final int USSD_MODE_NOTIFY = 0; 60 public static final int USSD_MODE_REQUEST = 1; 61 62 private static final String TAG = "ImsCall"; 63 64 // This flag is meant to be used as a debugging tool to quickly see all logs 65 // regardless of the actual log level set on this component. 66 private static final boolean FORCE_DEBUG = false; /* STOPSHIP if true */ 67 68 // We will log messages guarded by these flags at the info level. If logging is required 69 // to occur at (and only at) a particular log level, please use the logd, logv and loge 70 // functions as those will not be affected by the value of FORCE_DEBUG at all. 71 // Otherwise, anything guarded by these flags will be logged at the info level since that 72 // level allows those statements ot be logged by default which supports the workflow of 73 // setting FORCE_DEBUG and knowing these logs will show up regardless of the actual log 74 // level of this component. 75 private static final boolean DBG = FORCE_DEBUG || Log.isLoggable(TAG, Log.DEBUG); 76 private static final boolean VDBG = FORCE_DEBUG || Log.isLoggable(TAG, Log.VERBOSE); 77 // This is a special flag that is used only to highlight specific log around bringing 78 // up and tearing down conference calls. At times, these errors are transient and hard to 79 // reproduce so we need to capture this information the first time. 80 // TODO: Set this flag to FORCE_DEBUG once the new conference call logic gets more mileage 81 // across different IMS implementations. 82 private static final boolean CONF_DBG = true; 83 84 private List<ConferenceParticipant> mConferenceParticipants; 85 /** 86 * Listener for events relating to an IMS call, such as when a call is being 87 * received ("on ringing") or a call is outgoing ("on calling"). 88 * <p>Many of these events are also received by {@link ImsCallSession.Listener}.</p> 89 */ 90 public static class Listener { 91 /** 92 * Called when a request is sent out to initiate a new call 93 * and 1xx response is received from the network. 94 * The default implementation calls {@link #onCallStateChanged}. 95 * 96 * @param call the call object that carries out the IMS call 97 */ 98 public void onCallProgressing(ImsCall call) { 99 onCallStateChanged(call); 100 } 101 102 /** 103 * Called when the call is established. 104 * The default implementation calls {@link #onCallStateChanged}. 105 * 106 * @param call the call object that carries out the IMS call 107 */ 108 public void onCallStarted(ImsCall call) { 109 onCallStateChanged(call); 110 } 111 112 /** 113 * Called when the call setup is failed. 114 * The default implementation calls {@link #onCallError}. 115 * 116 * @param call the call object that carries out the IMS call 117 * @param reasonInfo detailed reason of the call setup failure 118 */ 119 public void onCallStartFailed(ImsCall call, ImsReasonInfo reasonInfo) { 120 onCallError(call, reasonInfo); 121 } 122 123 /** 124 * Called when the call is terminated. 125 * The default implementation calls {@link #onCallStateChanged}. 126 * 127 * @param call the call object that carries out the IMS call 128 * @param reasonInfo detailed reason of the call termination 129 */ 130 public void onCallTerminated(ImsCall call, ImsReasonInfo reasonInfo) { 131 // Store the call termination reason 132 133 onCallStateChanged(call); 134 } 135 136 /** 137 * Called when the call is in hold. 138 * The default implementation calls {@link #onCallStateChanged}. 139 * 140 * @param call the call object that carries out the IMS call 141 */ 142 public void onCallHeld(ImsCall call) { 143 onCallStateChanged(call); 144 } 145 146 /** 147 * Called when the call hold is failed. 148 * The default implementation calls {@link #onCallError}. 149 * 150 * @param call the call object that carries out the IMS call 151 * @param reasonInfo detailed reason of the call hold failure 152 */ 153 public void onCallHoldFailed(ImsCall call, ImsReasonInfo reasonInfo) { 154 onCallError(call, reasonInfo); 155 } 156 157 /** 158 * Called when the call hold is received from the remote user. 159 * The default implementation calls {@link #onCallStateChanged}. 160 * 161 * @param call the call object that carries out the IMS call 162 */ 163 public void onCallHoldReceived(ImsCall call) { 164 onCallStateChanged(call); 165 } 166 167 /** 168 * Called when the call is in call. 169 * The default implementation calls {@link #onCallStateChanged}. 170 * 171 * @param call the call object that carries out the IMS call 172 */ 173 public void onCallResumed(ImsCall call) { 174 onCallStateChanged(call); 175 } 176 177 /** 178 * Called when the call resume is failed. 179 * The default implementation calls {@link #onCallError}. 180 * 181 * @param call the call object that carries out the IMS call 182 * @param reasonInfo detailed reason of the call resume failure 183 */ 184 public void onCallResumeFailed(ImsCall call, ImsReasonInfo reasonInfo) { 185 onCallError(call, reasonInfo); 186 } 187 188 /** 189 * Called when the call resume is received from the remote user. 190 * The default implementation calls {@link #onCallStateChanged}. 191 * 192 * @param call the call object that carries out the IMS call 193 */ 194 public void onCallResumeReceived(ImsCall call) { 195 onCallStateChanged(call); 196 } 197 198 /** 199 * Called when the call is in call. 200 * The default implementation calls {@link #onCallStateChanged}. 201 * 202 * @param call the call object that carries out the active IMS call 203 * @param peerCall the call object that carries out the held IMS call 204 * @param swapCalls {@code true} if the foreground and background calls should be swapped 205 * now that the merge has completed. 206 */ 207 public void onCallMerged(ImsCall call, ImsCall peerCall, boolean swapCalls) { 208 onCallStateChanged(call); 209 } 210 211 /** 212 * Called when the call merge is failed. 213 * The default implementation calls {@link #onCallError}. 214 * 215 * @param call the call object that carries out the IMS call 216 * @param reasonInfo detailed reason of the call merge failure 217 */ 218 public void onCallMergeFailed(ImsCall call, ImsReasonInfo reasonInfo) { 219 onCallError(call, reasonInfo); 220 } 221 222 /** 223 * Called when the call is updated (except for hold/unhold). 224 * The default implementation calls {@link #onCallStateChanged}. 225 * 226 * @param call the call object that carries out the IMS call 227 */ 228 public void onCallUpdated(ImsCall call) { 229 onCallStateChanged(call); 230 } 231 232 /** 233 * Called when the call update is failed. 234 * The default implementation calls {@link #onCallError}. 235 * 236 * @param call the call object that carries out the IMS call 237 * @param reasonInfo detailed reason of the call update failure 238 */ 239 public void onCallUpdateFailed(ImsCall call, ImsReasonInfo reasonInfo) { 240 onCallError(call, reasonInfo); 241 } 242 243 /** 244 * Called when the call update is received from the remote user. 245 * 246 * @param call the call object that carries out the IMS call 247 */ 248 public void onCallUpdateReceived(ImsCall call) { 249 // no-op 250 } 251 252 /** 253 * Called when the call is extended to the conference call. 254 * The default implementation calls {@link #onCallStateChanged}. 255 * 256 * @param call the call object that carries out the IMS call 257 * @param newCall the call object that is extended to the conference from the active call 258 */ 259 public void onCallConferenceExtended(ImsCall call, ImsCall newCall) { 260 onCallStateChanged(call); 261 } 262 263 /** 264 * Called when the conference extension is failed. 265 * The default implementation calls {@link #onCallError}. 266 * 267 * @param call the call object that carries out the IMS call 268 * @param reasonInfo detailed reason of the conference extension failure 269 */ 270 public void onCallConferenceExtendFailed(ImsCall call, 271 ImsReasonInfo reasonInfo) { 272 onCallError(call, reasonInfo); 273 } 274 275 /** 276 * Called when the conference extension is received from the remote user. 277 * 278 * @param call the call object that carries out the IMS call 279 * @param newCall the call object that is extended to the conference from the active call 280 */ 281 public void onCallConferenceExtendReceived(ImsCall call, ImsCall newCall) { 282 onCallStateChanged(call); 283 } 284 285 /** 286 * Called when the invitation request of the participants is delivered to 287 * the conference server. 288 * 289 * @param call the call object that carries out the IMS call 290 */ 291 public void onCallInviteParticipantsRequestDelivered(ImsCall call) { 292 // no-op 293 } 294 295 /** 296 * Called when the invitation request of the participants is failed. 297 * 298 * @param call the call object that carries out the IMS call 299 * @param reasonInfo detailed reason of the conference invitation failure 300 */ 301 public void onCallInviteParticipantsRequestFailed(ImsCall call, 302 ImsReasonInfo reasonInfo) { 303 // no-op 304 } 305 306 /** 307 * Called when the removal request of the participants is delivered to 308 * the conference server. 309 * 310 * @param call the call object that carries out the IMS call 311 */ 312 public void onCallRemoveParticipantsRequestDelivered(ImsCall call) { 313 // no-op 314 } 315 316 /** 317 * Called when the removal request of the participants is failed. 318 * 319 * @param call the call object that carries out the IMS call 320 * @param reasonInfo detailed reason of the conference removal failure 321 */ 322 public void onCallRemoveParticipantsRequestFailed(ImsCall call, 323 ImsReasonInfo reasonInfo) { 324 // no-op 325 } 326 327 /** 328 * Called when the conference state is updated. 329 * 330 * @param call the call object that carries out the IMS call 331 * @param state state of the participant who is participated in the conference call 332 */ 333 public void onCallConferenceStateUpdated(ImsCall call, ImsConferenceState state) { 334 // no-op 335 } 336 337 /** 338 * Called when the state of IMS conference participant(s) has changed. 339 * 340 * @param call the call object that carries out the IMS call. 341 * @param participants the participant(s) and their new state information. 342 */ 343 public void onConferenceParticipantsStateChanged(ImsCall call, 344 List<ConferenceParticipant> participants) { 345 // no-op 346 } 347 348 /** 349 * Called when the USSD message is received from the network. 350 * 351 * @param mode mode of the USSD message (REQUEST / NOTIFY) 352 * @param ussdMessage USSD message 353 */ 354 public void onCallUssdMessageReceived(ImsCall call, 355 int mode, String ussdMessage) { 356 // no-op 357 } 358 359 /** 360 * Called when an error occurs. The default implementation is no op. 361 * overridden. The default implementation is no op. Error events are 362 * not re-directed to this callback and are handled in {@link #onCallError}. 363 * 364 * @param call the call object that carries out the IMS call 365 * @param reasonInfo detailed reason of this error 366 * @see ImsReasonInfo 367 */ 368 public void onCallError(ImsCall call, ImsReasonInfo reasonInfo) { 369 // no-op 370 } 371 372 /** 373 * Called when an event occurs and the corresponding callback is not 374 * overridden. The default implementation is no op. Error events are 375 * not re-directed to this callback and are handled in {@link #onCallError}. 376 * 377 * @param call the call object that carries out the IMS call 378 */ 379 public void onCallStateChanged(ImsCall call) { 380 // no-op 381 } 382 383 /** 384 * Called when the call moves the hold state to the conversation state. 385 * For example, when merging the active & hold call, the state of all the hold call 386 * will be changed from hold state to conversation state. 387 * This callback method can be invoked even though the application does not trigger 388 * any operations. 389 * 390 * @param call the call object that carries out the IMS call 391 * @param state the detailed state of call state changes; 392 * Refer to CALL_STATE_* in {@link ImsCall} 393 */ 394 public void onCallStateChanged(ImsCall call, int state) { 395 // no-op 396 } 397 398 /** 399 * Called when the call supp service is received 400 * The default implementation calls {@link #onCallStateChanged}. 401 * 402 * @param call the call object that carries out the IMS call 403 */ 404 public void onCallSuppServiceReceived(ImsCall call, 405 ImsSuppServiceNotification suppServiceInfo) { 406 } 407 408 /** 409 * Called when TTY mode of remote party changed 410 * 411 * @param call the call object that carries out the IMS call 412 * @param mode TTY mode of remote party 413 */ 414 public void onCallSessionTtyModeReceived(ImsCall call, int mode) { 415 // no-op 416 } 417 418 /** 419 * Called when handover occurs from one access technology to another. 420 * 421 * @param imsCall ImsCall object 422 * @param srcAccessTech original access technology 423 * @param targetAccessTech new access technology 424 * @param reasonInfo 425 */ 426 public void onCallHandover(ImsCall imsCall, int srcAccessTech, int targetAccessTech, 427 ImsReasonInfo reasonInfo) { 428 } 429 430 /** 431 * Called when the remote party issues an RTT modify request 432 * 433 * @param imsCall ImsCall object 434 */ 435 public void onRttModifyRequestReceived(ImsCall imsCall) { 436 } 437 438 /** 439 * Called when the remote party responds to a locally-issued RTT request. 440 * 441 * @param imsCall ImsCall object 442 * @param status The status of the request. See 443 * {@link Connection.RttModifyStatus} for possible values. 444 */ 445 public void onRttModifyResponseReceived(ImsCall imsCall, int status) { 446 } 447 448 /** 449 * Called when the remote party has sent some characters via RTT 450 * 451 * @param imsCall ImsCall object 452 * @param message A string containing the transmitted characters. 453 */ 454 public void onRttMessageReceived(ImsCall imsCall, String message) { 455 } 456 457 /** 458 * Called when handover from one access technology to another fails. 459 * 460 * @param imsCall call that failed the handover. 461 * @param srcAccessTech original access technology 462 * @param targetAccessTech new access technology 463 * @param reasonInfo 464 */ 465 public void onCallHandoverFailed(ImsCall imsCall, int srcAccessTech, int targetAccessTech, 466 ImsReasonInfo reasonInfo) { 467 } 468 469 /** 470 * Notifies of a change to the multiparty state for this {@code ImsCall}. 471 * 472 * @param imsCall The IMS call. 473 * @param isMultiParty {@code true} if the call became multiparty, {@code false} 474 * otherwise. 475 */ 476 public void onMultipartyStateChanged(ImsCall imsCall, boolean isMultiParty) { 477 } 478 } 479 480 // List of update operation for IMS call control 481 private static final int UPDATE_NONE = 0; 482 private static final int UPDATE_HOLD = 1; 483 private static final int UPDATE_HOLD_MERGE = 2; 484 private static final int UPDATE_RESUME = 3; 485 private static final int UPDATE_MERGE = 4; 486 private static final int UPDATE_EXTEND_TO_CONFERENCE = 5; 487 private static final int UPDATE_UNSPECIFIED = 6; 488 489 // For synchronization of private variables 490 private Object mLockObj = new Object(); 491 private Context mContext; 492 493 // true if the call is established & in the conversation state 494 private boolean mInCall = false; 495 // true if the call is on hold 496 // If it is triggered by the local, mute the call. Otherwise, play local hold tone 497 // or network generated media. 498 private boolean mHold = false; 499 // true if the call is on mute 500 private boolean mMute = false; 501 // It contains the exclusive call update request. Refer to UPDATE_*. 502 private int mUpdateRequest = UPDATE_NONE; 503 504 private ImsCall.Listener mListener = null; 505 506 // When merging two calls together, the "peer" call that will merge into this call. 507 private ImsCall mMergePeer = null; 508 // When merging two calls together, the "host" call we are merging into. 509 private ImsCall mMergeHost = null; 510 511 // True if Conference request was initiated by 512 // Foreground Conference call else it will be false 513 private boolean mMergeRequestedByConference = false; 514 // Wrapper call session to interworking the IMS service (server). 515 private ImsCallSession mSession = null; 516 // Call profile of the current session. 517 // It can be changed at anytime when the call is updated. 518 private ImsCallProfile mCallProfile = null; 519 // Call profile to be updated after the application's action (accept/reject) 520 // to the call update. After the application's action (accept/reject) is done, 521 // it will be set to null. 522 private ImsCallProfile mProposedCallProfile = null; 523 private ImsReasonInfo mLastReasonInfo = null; 524 525 // Media session to control media (audio/video) operations for an IMS call 526 private ImsStreamMediaSession mMediaSession = null; 527 528 // The temporary ImsCallSession that could represent the merged call once 529 // we receive notification that the merge was successful. 530 private ImsCallSession mTransientConferenceSession = null; 531 // While a merge is progressing, we bury any session termination requests 532 // made on the original ImsCallSession until we have closure on the merge request 533 // If the request ultimately fails, we need to act on the termination request 534 // that we buried temporarily. We do this because we feel that timing issues could 535 // cause the termination request to occur just because the merge is succeeding. 536 private boolean mSessionEndDuringMerge = false; 537 // Just like mSessionEndDuringMerge, we need to keep track of the reason why the 538 // termination request was made on the original session in case we need to act 539 // on it in the case of a merge failure. 540 private ImsReasonInfo mSessionEndDuringMergeReasonInfo = null; 541 // This flag is used to indicate if this ImsCall was merged into a conference 542 // or not. It is used primarily to determine if a disconnect sound should 543 // be heard when the call is terminated. 544 private boolean mIsMerged = false; 545 // If true, this flag means that this ImsCall is in the process of merging 546 // into a conference but it does not yet have closure on if it was 547 // actually added to the conference or not. false implies that it either 548 // is not part of a merging conference or already knows if it was 549 // successfully added. 550 private boolean mCallSessionMergePending = false; 551 552 /** 553 * If {@code true}, this flag indicates that a request to terminate the call was made by 554 * Telephony (could be from the user or some internal telephony logic) 555 * and that when we receive a {@link #processCallTerminated(ImsReasonInfo)} callback from the 556 * radio indicating that the call was terminated, we should override any burying of the 557 * termination due to an ongoing conference merge. 558 */ 559 private boolean mTerminationRequestPending = false; 560 561 /** 562 * For multi-party IMS calls (e.g. conferences), determines if this {@link ImsCall} is the one 563 * hosting the call. This is used to distinguish between a situation where an {@link ImsCall} 564 * is {@link #isMultiparty()} because calls were merged on the device, and a situation where 565 * an {@link ImsCall} is {@link #isMultiparty()} because it is a member of a conference started 566 * on another device. 567 * <p> 568 * When {@code true}, this {@link ImsCall} is is the origin of the conference call. 569 * When {@code false}, this {@link ImsCall} is a member of a conference started on another 570 * device. 571 */ 572 private boolean mIsConferenceHost = false; 573 574 /** 575 * Tracks whether this {@link ImsCall} has been a video call at any point in its lifetime. 576 * Some examples of calls which are/were video calls: 577 * 1. A call which has been a video call for its duration. 578 * 2. An audio call upgraded to video (and potentially downgraded to audio later). 579 * 3. A call answered as video which was downgraded to audio. 580 */ 581 private boolean mWasVideoCall = false; 582 583 /** 584 * Unique id generator used to generate call id. 585 */ 586 private static final AtomicInteger sUniqueIdGenerator = new AtomicInteger(); 587 588 /** 589 * Unique identifier. 590 */ 591 public final int uniqueId; 592 593 /** 594 * The current ImsCallSessionListenerProxy. 595 */ 596 private ImsCallSessionListenerProxy mImsCallSessionListenerProxy; 597 598 /** 599 * When calling {@link #terminate(int, int)}, an override for the termination reason which the 600 * modem returns. 601 * 602 * Necessary because passing in an unexpected {@link ImsReasonInfo} reason code to 603 * {@link #terminate(int)} will cause the modem to ignore the terminate request. 604 */ 605 private int mOverrideReason = ImsReasonInfo.CODE_UNSPECIFIED; 606 607 /** 608 * When true, if this call is incoming, it will be answered with an 609 * {@link ImsStreamMediaProfile} that has RTT enabled. 610 */ 611 private boolean mAnswerWithRtt = false; 612 613 /** 614 * Create an IMS call object. 615 * 616 * @param context the context for accessing system services 617 * @param profile the call profile to make/take a call 618 */ 619 public ImsCall(Context context, ImsCallProfile profile) { 620 mContext = context; 621 setCallProfile(profile); 622 uniqueId = sUniqueIdGenerator.getAndIncrement(); 623 } 624 625 /** 626 * Closes this object. This object is not usable after being closed. 627 */ 628 @Override 629 public void close() { 630 synchronized(mLockObj) { 631 if (mSession != null) { 632 mSession.close(); 633 mSession = null; 634 } else { 635 logi("close :: Cannot close Null call session!"); 636 } 637 638 mCallProfile = null; 639 mProposedCallProfile = null; 640 mLastReasonInfo = null; 641 mMediaSession = null; 642 } 643 } 644 645 /** 646 * Checks if the call has a same remote user identity or not. 647 * 648 * @param userId the remote user identity 649 * @return true if the remote user identity is equal; otherwise, false 650 */ 651 @Override 652 public boolean checkIfRemoteUserIsSame(String userId) { 653 if (userId == null) { 654 return false; 655 } 656 657 return userId.equals(mCallProfile.getCallExtra(ImsCallProfile.EXTRA_REMOTE_URI, "")); 658 } 659 660 /** 661 * Checks if the call is equal or not. 662 * 663 * @param call the call to be compared 664 * @return true if the call is equal; otherwise, false 665 */ 666 @Override 667 public boolean equalsTo(ICall call) { 668 if (call == null) { 669 return false; 670 } 671 672 if (call instanceof ImsCall) { 673 return this.equals(call); 674 } 675 676 return false; 677 } 678 679 public static boolean isSessionAlive(ImsCallSession session) { 680 return session != null && session.isAlive(); 681 } 682 683 /** 684 * Gets the negotiated (local & remote) call profile. 685 * 686 * @return a {@link ImsCallProfile} object that has the negotiated call profile 687 */ 688 public ImsCallProfile getCallProfile() { 689 synchronized(mLockObj) { 690 return mCallProfile; 691 } 692 } 693 694 /** 695 * Replaces the current call profile with a new one, tracking whethere this was previously a 696 * video call or not. 697 * 698 * @param profile The new call profile. 699 */ 700 @VisibleForTesting 701 public void setCallProfile(ImsCallProfile profile) { 702 synchronized(mLockObj) { 703 mCallProfile = profile; 704 trackVideoStateHistory(mCallProfile); 705 } 706 } 707 708 /** 709 * Gets the local call profile (local capabilities). 710 * 711 * @return a {@link ImsCallProfile} object that has the local call profile 712 */ 713 public ImsCallProfile getLocalCallProfile() throws ImsException { 714 synchronized(mLockObj) { 715 if (mSession == null) { 716 throw new ImsException("No call session", 717 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 718 } 719 720 try { 721 return mSession.getLocalCallProfile(); 722 } catch (Throwable t) { 723 loge("getLocalCallProfile :: ", t); 724 throw new ImsException("getLocalCallProfile()", t, 0); 725 } 726 } 727 } 728 729 /** 730 * Gets the remote call profile (remote capabilities). 731 * 732 * @return a {@link ImsCallProfile} object that has the remote call profile 733 */ 734 public ImsCallProfile getRemoteCallProfile() throws ImsException { 735 synchronized(mLockObj) { 736 if (mSession == null) { 737 throw new ImsException("No call session", 738 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 739 } 740 741 try { 742 return mSession.getRemoteCallProfile(); 743 } catch (Throwable t) { 744 loge("getRemoteCallProfile :: ", t); 745 throw new ImsException("getRemoteCallProfile()", t, 0); 746 } 747 } 748 } 749 750 /** 751 * Gets the call profile proposed by the local/remote user. 752 * 753 * @return a {@link ImsCallProfile} object that has the proposed call profile 754 */ 755 public ImsCallProfile getProposedCallProfile() { 756 synchronized(mLockObj) { 757 if (!isInCall()) { 758 return null; 759 } 760 761 return mProposedCallProfile; 762 } 763 } 764 765 /** 766 * Gets the list of conference participants currently 767 * associated with this call. 768 * 769 * @return Copy of the list of conference participants. 770 */ 771 public List<ConferenceParticipant> getConferenceParticipants() { 772 synchronized(mLockObj) { 773 logi("getConferenceParticipants :: mConferenceParticipants" 774 + mConferenceParticipants); 775 if (mConferenceParticipants == null) { 776 return null; 777 } 778 if (mConferenceParticipants.isEmpty()) { 779 return new ArrayList<ConferenceParticipant>(0); 780 } 781 return new ArrayList<ConferenceParticipant>(mConferenceParticipants); 782 } 783 } 784 785 /** 786 * Gets the state of the {@link ImsCallSession} that carries this call. 787 * The value returned must be one of the states in {@link ImsCallSession#State}. 788 * 789 * @return the session state 790 */ 791 public int getState() { 792 synchronized(mLockObj) { 793 if (mSession == null) { 794 return ImsCallSession.State.IDLE; 795 } 796 797 return mSession.getState(); 798 } 799 } 800 801 /** 802 * Gets the {@link ImsCallSession} that carries this call. 803 * 804 * @return the session object that carries this call 805 * @hide 806 */ 807 public ImsCallSession getCallSession() { 808 synchronized(mLockObj) { 809 return mSession; 810 } 811 } 812 813 /** 814 * Gets the {@link ImsStreamMediaSession} that handles the media operation of this call. 815 * Almost interface APIs are for the VT (Video Telephony). 816 * 817 * @return the media session object that handles the media operation of this call 818 * @hide 819 */ 820 public ImsStreamMediaSession getMediaSession() { 821 synchronized(mLockObj) { 822 return mMediaSession; 823 } 824 } 825 826 /** 827 * Gets the specified property of this call. 828 * 829 * @param name key to get the extra call information defined in {@link ImsCallProfile} 830 * @return the extra call information as string 831 */ 832 public String getCallExtra(String name) throws ImsException { 833 // Lookup the cache 834 835 synchronized(mLockObj) { 836 // If not found, try to get the property from the remote 837 if (mSession == null) { 838 throw new ImsException("No call session", 839 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 840 } 841 842 try { 843 return mSession.getProperty(name); 844 } catch (Throwable t) { 845 loge("getCallExtra :: ", t); 846 throw new ImsException("getCallExtra()", t, 0); 847 } 848 } 849 } 850 851 /** 852 * Gets the last reason information when the call is not established, cancelled or terminated. 853 * 854 * @return the last reason information 855 */ 856 public ImsReasonInfo getLastReasonInfo() { 857 synchronized(mLockObj) { 858 return mLastReasonInfo; 859 } 860 } 861 862 /** 863 * Checks if the call has a pending update operation. 864 * 865 * @return true if the call has a pending update operation 866 */ 867 public boolean hasPendingUpdate() { 868 synchronized(mLockObj) { 869 return (mUpdateRequest != UPDATE_NONE); 870 } 871 } 872 873 /** 874 * Checks if the call is pending a hold operation. 875 * 876 * @return true if the call is pending a hold operation. 877 */ 878 public boolean isPendingHold() { 879 synchronized(mLockObj) { 880 return (mUpdateRequest == UPDATE_HOLD); 881 } 882 } 883 884 /** 885 * Checks if the call is established. 886 * 887 * @return true if the call is established 888 */ 889 public boolean isInCall() { 890 synchronized(mLockObj) { 891 return mInCall; 892 } 893 } 894 895 /** 896 * Checks if the call is muted. 897 * 898 * @return true if the call is muted 899 */ 900 public boolean isMuted() { 901 synchronized(mLockObj) { 902 return mMute; 903 } 904 } 905 906 /** 907 * Checks if the call is on hold. 908 * 909 * @return true if the call is on hold 910 */ 911 public boolean isOnHold() { 912 synchronized(mLockObj) { 913 return mHold; 914 } 915 } 916 917 /** 918 * Determines if the call is a multiparty call. 919 * 920 * @return {@code True} if the call is a multiparty call. 921 */ 922 public boolean isMultiparty() { 923 synchronized(mLockObj) { 924 if (mSession == null) { 925 return false; 926 } 927 928 return mSession.isMultiparty(); 929 } 930 } 931 932 /** 933 * Where {@link #isMultiparty()} is {@code true}, determines if this {@link ImsCall} is the 934 * origin of the conference call (i.e. {@code #isConferenceHost()} is {@code true}), or if this 935 * {@link ImsCall} is a member of a conference hosted on another device. 936 * 937 * @return {@code true} if this call is the origin of the conference call it is a member of, 938 * {@code false} otherwise. 939 */ 940 public boolean isConferenceHost() { 941 synchronized(mLockObj) { 942 return isMultiparty() && mIsConferenceHost; 943 } 944 } 945 946 /** 947 * Marks whether an IMS call is merged. This should be set {@code true} when the call merges 948 * into a conference. 949 * 950 * @param isMerged Whether the call is merged. 951 */ 952 public void setIsMerged(boolean isMerged) { 953 mIsMerged = isMerged; 954 } 955 956 /** 957 * @return {@code true} if the call recently merged into a conference call. 958 */ 959 public boolean isMerged() { 960 return mIsMerged; 961 } 962 963 /** 964 * Sets the listener to listen to the IMS call events. 965 * The method calls {@link #setListener setListener(listener, false)}. 966 * 967 * @param listener to listen to the IMS call events of this object; null to remove listener 968 * @see #setListener(Listener, boolean) 969 */ 970 public void setListener(ImsCall.Listener listener) { 971 setListener(listener, false); 972 } 973 974 /** 975 * Sets the listener to listen to the IMS call events. 976 * A {@link ImsCall} can only hold one listener at a time. Subsequent calls 977 * to this method override the previous listener. 978 * 979 * @param listener to listen to the IMS call events of this object; null to remove listener 980 * @param callbackImmediately set to true if the caller wants to be called 981 * back immediately on the current state 982 */ 983 public void setListener(ImsCall.Listener listener, boolean callbackImmediately) { 984 boolean inCall; 985 boolean onHold; 986 int state; 987 ImsReasonInfo lastReasonInfo; 988 989 synchronized(mLockObj) { 990 mListener = listener; 991 992 if ((listener == null) || !callbackImmediately) { 993 return; 994 } 995 996 inCall = mInCall; 997 onHold = mHold; 998 state = getState(); 999 lastReasonInfo = mLastReasonInfo; 1000 } 1001 1002 try { 1003 if (lastReasonInfo != null) { 1004 listener.onCallError(this, lastReasonInfo); 1005 } else if (inCall) { 1006 if (onHold) { 1007 listener.onCallHeld(this); 1008 } else { 1009 listener.onCallStarted(this); 1010 } 1011 } else { 1012 switch (state) { 1013 case ImsCallSession.State.ESTABLISHING: 1014 listener.onCallProgressing(this); 1015 break; 1016 case ImsCallSession.State.TERMINATED: 1017 listener.onCallTerminated(this, lastReasonInfo); 1018 break; 1019 default: 1020 // Ignore it. There is no action in the other state. 1021 break; 1022 } 1023 } 1024 } catch (Throwable t) { 1025 loge("setListener() :: ", t); 1026 } 1027 } 1028 1029 /** 1030 * Mutes or unmutes the mic for the active call. 1031 * 1032 * @param muted true if the call is muted, false otherwise 1033 */ 1034 public void setMute(boolean muted) throws ImsException { 1035 synchronized(mLockObj) { 1036 if (mMute != muted) { 1037 logi("setMute :: turning mute " + (muted ? "on" : "off")); 1038 mMute = muted; 1039 1040 try { 1041 mSession.setMute(muted); 1042 } catch (Throwable t) { 1043 loge("setMute :: ", t); 1044 throwImsException(t, 0); 1045 } 1046 } 1047 } 1048 } 1049 1050 /** 1051 * Attaches an incoming call to this call object. 1052 * 1053 * @param session the session that receives the incoming call 1054 * @throws ImsException if the IMS service fails to attach this object to the session 1055 */ 1056 public void attachSession(ImsCallSession session) throws ImsException { 1057 logi("attachSession :: session=" + session); 1058 1059 synchronized(mLockObj) { 1060 mSession = session; 1061 1062 try { 1063 mSession.setListener(createCallSessionListener()); 1064 } catch (Throwable t) { 1065 loge("attachSession :: ", t); 1066 throwImsException(t, 0); 1067 } 1068 } 1069 } 1070 1071 /** 1072 * Initiates an IMS call with the call profile which is provided 1073 * when creating a {@link ImsCall}. 1074 * 1075 * @param session the {@link ImsCallSession} for carrying out the call 1076 * @param callee callee information to initiate an IMS call 1077 * @throws ImsException if the IMS service fails to initiate the call 1078 */ 1079 public void start(ImsCallSession session, String callee) 1080 throws ImsException { 1081 logi("start(1) :: session=" + session); 1082 1083 synchronized(mLockObj) { 1084 mSession = session; 1085 1086 try { 1087 session.setListener(createCallSessionListener()); 1088 session.start(callee, mCallProfile); 1089 } catch (Throwable t) { 1090 loge("start(1) :: ", t); 1091 throw new ImsException("start(1)", t, 0); 1092 } 1093 } 1094 } 1095 1096 /** 1097 * Initiates an IMS conferenca call with the call profile which is provided 1098 * when creating a {@link ImsCall}. 1099 * 1100 * @param session the {@link ImsCallSession} for carrying out the call 1101 * @param participants participant list to initiate an IMS conference call 1102 * @throws ImsException if the IMS service fails to initiate the call 1103 */ 1104 public void start(ImsCallSession session, String[] participants) 1105 throws ImsException { 1106 logi("start(n) :: session=" + session); 1107 1108 synchronized(mLockObj) { 1109 mSession = session; 1110 1111 try { 1112 session.setListener(createCallSessionListener()); 1113 session.start(participants, mCallProfile); 1114 } catch (Throwable t) { 1115 loge("start(n) :: ", t); 1116 throw new ImsException("start(n)", t, 0); 1117 } 1118 } 1119 } 1120 1121 /** 1122 * Accepts a call. 1123 * 1124 * @see Listener#onCallStarted 1125 * 1126 * @param callType The call type the user agreed to for accepting the call. 1127 * @throws ImsException if the IMS service fails to accept the call 1128 */ 1129 public void accept(int callType) throws ImsException { 1130 accept(callType, new ImsStreamMediaProfile()); 1131 } 1132 1133 /** 1134 * Accepts a call. 1135 * 1136 * @param callType call type to be answered in {@link ImsCallProfile} 1137 * @param profile a media profile to be answered (audio/audio & video, direction, ...) 1138 * @see Listener#onCallStarted 1139 * @throws ImsException if the IMS service fails to accept the call 1140 */ 1141 public void accept(int callType, ImsStreamMediaProfile profile) throws ImsException { 1142 logi("accept :: callType=" + callType + ", profile=" + profile); 1143 1144 if (mAnswerWithRtt) { 1145 profile.mRttMode = ImsStreamMediaProfile.RTT_MODE_FULL; 1146 logi("accept :: changing media profile RTT mode to full"); 1147 } 1148 1149 synchronized(mLockObj) { 1150 if (mSession == null) { 1151 throw new ImsException("No call to answer", 1152 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 1153 } 1154 1155 try { 1156 mSession.accept(callType, profile); 1157 } catch (Throwable t) { 1158 loge("accept :: ", t); 1159 throw new ImsException("accept()", t, 0); 1160 } 1161 1162 if (mInCall && (mProposedCallProfile != null)) { 1163 if (DBG) { 1164 logi("accept :: call profile will be updated"); 1165 } 1166 1167 mCallProfile = mProposedCallProfile; 1168 trackVideoStateHistory(mCallProfile); 1169 mProposedCallProfile = null; 1170 } 1171 1172 // Other call update received 1173 if (mInCall && (mUpdateRequest == UPDATE_UNSPECIFIED)) { 1174 mUpdateRequest = UPDATE_NONE; 1175 } 1176 } 1177 } 1178 1179 /** 1180 * Deflects a call. 1181 * 1182 * @param number number to be deflected to. 1183 * @throws ImsException if the IMS service fails to deflect the call 1184 */ 1185 public void deflect(String number) throws ImsException { 1186 logi("deflect :: session=" + mSession + ", number=" + Rlog.pii(TAG, number)); 1187 1188 synchronized(mLockObj) { 1189 if (mSession == null) { 1190 throw new ImsException("No call to deflect", 1191 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 1192 } 1193 1194 try { 1195 mSession.deflect(number); 1196 } catch (Throwable t) { 1197 loge("deflect :: ", t); 1198 throw new ImsException("deflect()", t, 0); 1199 } 1200 } 1201 } 1202 1203 /** 1204 * Rejects a call. 1205 * 1206 * @param reason reason code to reject an incoming call 1207 * @see Listener#onCallStartFailed 1208 * @throws ImsException if the IMS service fails to reject the call 1209 */ 1210 public void reject(int reason) throws ImsException { 1211 logi("reject :: reason=" + reason); 1212 1213 synchronized(mLockObj) { 1214 if (mSession != null) { 1215 mSession.reject(reason); 1216 } 1217 1218 if (mInCall && (mProposedCallProfile != null)) { 1219 if (DBG) { 1220 logi("reject :: call profile is not updated; destroy it..."); 1221 } 1222 1223 mProposedCallProfile = null; 1224 } 1225 1226 // Other call update received 1227 if (mInCall && (mUpdateRequest == UPDATE_UNSPECIFIED)) { 1228 mUpdateRequest = UPDATE_NONE; 1229 } 1230 } 1231 } 1232 1233 public void terminate(int reason, int overrideReason) throws ImsException { 1234 logi("terminate :: reason=" + reason + " ; overrideReadon=" + overrideReason); 1235 mOverrideReason = overrideReason; 1236 terminate(reason); 1237 } 1238 1239 /** 1240 * Terminates an IMS call (e.g. user initiated). 1241 * 1242 * @param reason reason code to terminate a call 1243 * @throws ImsException if the IMS service fails to terminate the call 1244 */ 1245 public void terminate(int reason) throws ImsException { 1246 logi("terminate :: reason=" + reason); 1247 1248 synchronized(mLockObj) { 1249 mHold = false; 1250 mInCall = false; 1251 mTerminationRequestPending = true; 1252 1253 if (mSession != null) { 1254 // TODO: Fix the fact that user invoked call terminations during 1255 // the process of establishing a conference call needs to be handled 1256 // as a special case. 1257 // Currently, any terminations (both invoked by the user or 1258 // by the network results in a callSessionTerminated() callback 1259 // from the network. When establishing a conference call we bury 1260 // these callbacks until we get closure on all participants of the 1261 // conference. In some situations, we will throw away the callback 1262 // (when the underlying session of the host of the new conference 1263 // is terminated) or will will unbury it when the conference has been 1264 // established, like when the peer of the new conference goes away 1265 // after the conference has been created. The UI relies on the callback 1266 // to reflect the fact that the call is gone. 1267 // So if a user decides to terminated a call while it is merging, it 1268 // could take a long time to reflect in the UI due to the conference 1269 // processing but we should probably cancel that and just terminate 1270 // the call immediately and clean up. This is not a huge issue right 1271 // now because we have not seen instances where establishing a 1272 // conference takes a long time (more than a second or two). 1273 mSession.terminate(reason); 1274 } 1275 } 1276 } 1277 1278 1279 /** 1280 * Puts a call on hold. When succeeds, {@link Listener#onCallHeld} is called. 1281 * 1282 * @see Listener#onCallHeld, Listener#onCallHoldFailed 1283 * @throws ImsException if the IMS service fails to hold the call 1284 */ 1285 public void hold() throws ImsException { 1286 logi("hold :: "); 1287 1288 if (isOnHold()) { 1289 if (DBG) { 1290 logi("hold :: call is already on hold"); 1291 } 1292 return; 1293 } 1294 1295 synchronized(mLockObj) { 1296 if (mUpdateRequest != UPDATE_NONE) { 1297 loge("hold :: update is in progress; request=" + 1298 updateRequestToString(mUpdateRequest)); 1299 throw new ImsException("Call update is in progress", 1300 ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE); 1301 } 1302 1303 if (mSession == null) { 1304 throw new ImsException("No call session", 1305 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 1306 } 1307 1308 mSession.hold(createHoldMediaProfile()); 1309 // FIXME: We should update the state on the callback because that is where 1310 // we can confirm that the hold request was successful or not. 1311 mHold = true; 1312 mUpdateRequest = UPDATE_HOLD; 1313 } 1314 } 1315 1316 /** 1317 * Continues a call that's on hold. When succeeds, {@link Listener#onCallResumed} is called. 1318 * 1319 * @see Listener#onCallResumed, Listener#onCallResumeFailed 1320 * @throws ImsException if the IMS service fails to resume the call 1321 */ 1322 public void resume() throws ImsException { 1323 logi("resume :: "); 1324 1325 if (!isOnHold()) { 1326 if (DBG) { 1327 logi("resume :: call is not being held"); 1328 } 1329 return; 1330 } 1331 1332 synchronized(mLockObj) { 1333 if (mUpdateRequest != UPDATE_NONE) { 1334 loge("resume :: update is in progress; request=" + 1335 updateRequestToString(mUpdateRequest)); 1336 throw new ImsException("Call update is in progress", 1337 ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE); 1338 } 1339 1340 if (mSession == null) { 1341 loge("resume :: "); 1342 throw new ImsException("No call session", 1343 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 1344 } 1345 1346 // mHold is set to false in confirmation callback that the 1347 // ImsCall was resumed. 1348 mUpdateRequest = UPDATE_RESUME; 1349 mSession.resume(createResumeMediaProfile()); 1350 } 1351 } 1352 1353 /** 1354 * Merges the active & hold call. 1355 * 1356 * @see Listener#onCallMerged, Listener#onCallMergeFailed 1357 * @throws ImsException if the IMS service fails to merge the call 1358 */ 1359 private void merge() throws ImsException { 1360 logi("merge :: "); 1361 1362 synchronized(mLockObj) { 1363 // If the host of the merge is in the midst of some other operation, we cannot merge. 1364 if (mUpdateRequest != UPDATE_NONE) { 1365 setCallSessionMergePending(false); 1366 if (mMergePeer != null) { 1367 mMergePeer.setCallSessionMergePending(false); 1368 } 1369 loge("merge :: update is in progress; request=" + 1370 updateRequestToString(mUpdateRequest)); 1371 throw new ImsException("Call update is in progress", 1372 ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE); 1373 } 1374 1375 // The peer of the merge is in the midst of some other operation, we cannot merge. 1376 if (mMergePeer != null && mMergePeer.mUpdateRequest != UPDATE_NONE) { 1377 setCallSessionMergePending(false); 1378 mMergePeer.setCallSessionMergePending(false); 1379 loge("merge :: peer call update is in progress; request=" + 1380 updateRequestToString(mMergePeer.mUpdateRequest)); 1381 throw new ImsException("Peer call update is in progress", 1382 ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE); 1383 } 1384 1385 if (mSession == null) { 1386 loge("merge :: no call session"); 1387 throw new ImsException("No call session", 1388 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 1389 } 1390 1391 // if skipHoldBeforeMerge = true, IMS service implementation will 1392 // merge without explicitly holding the call. 1393 if (mHold || (mContext.getResources().getBoolean( 1394 com.android.internal.R.bool.skipHoldBeforeMerge))) { 1395 1396 if (mMergePeer != null && !mMergePeer.isMultiparty() && !isMultiparty()) { 1397 // We only set UPDATE_MERGE when we are adding the first 1398 // calls to the Conference. If there is already a conference 1399 // no special handling is needed. The existing conference 1400 // session will just go active and any other sessions will be terminated 1401 // if needed. There will be no merge failed callback. 1402 // Mark both the host and peer UPDATE_MERGE to ensure both are aware that a 1403 // merge is pending. 1404 mUpdateRequest = UPDATE_MERGE; 1405 mMergePeer.mUpdateRequest = UPDATE_MERGE; 1406 } 1407 1408 mSession.merge(); 1409 } else { 1410 // This code basically says, we need to explicitly hold before requesting a merge 1411 // when we get the callback that the hold was successful (or failed), we should 1412 // automatically request a merge. 1413 mSession.hold(createHoldMediaProfile()); 1414 mHold = true; 1415 mUpdateRequest = UPDATE_HOLD_MERGE; 1416 } 1417 } 1418 } 1419 1420 /** 1421 * Merges the active & hold call. 1422 * 1423 * @param bgCall the background (holding) call 1424 * @see Listener#onCallMerged, Listener#onCallMergeFailed 1425 * @throws ImsException if the IMS service fails to merge the call 1426 */ 1427 public void merge(ImsCall bgCall) throws ImsException { 1428 logi("merge(1) :: bgImsCall=" + bgCall); 1429 1430 if (bgCall == null) { 1431 throw new ImsException("No background call", 1432 ImsReasonInfo.CODE_LOCAL_ILLEGAL_ARGUMENT); 1433 } 1434 1435 synchronized(mLockObj) { 1436 // Mark both sessions as pending merge. 1437 this.setCallSessionMergePending(true); 1438 bgCall.setCallSessionMergePending(true); 1439 1440 if ((!isMultiparty() && !bgCall.isMultiparty()) || isMultiparty()) { 1441 // If neither call is multiparty, the current call is the merge host and the bg call 1442 // is the merge peer (ie we're starting a new conference). 1443 // OR 1444 // If this call is multiparty, it is the merge host and the other call is the merge 1445 // peer. 1446 setMergePeer(bgCall); 1447 } else { 1448 // If the bg call is multiparty, it is the merge host. 1449 setMergeHost(bgCall); 1450 } 1451 } 1452 1453 if (isMultiparty()) { 1454 mMergeRequestedByConference = true; 1455 } else { 1456 logi("merge : mMergeRequestedByConference not set"); 1457 } 1458 merge(); 1459 } 1460 1461 /** 1462 * Updates the current call's properties (ex. call mode change: video upgrade / downgrade). 1463 */ 1464 public void update(int callType, ImsStreamMediaProfile mediaProfile) throws ImsException { 1465 logi("update :: callType=" + callType + ", mediaProfile=" + mediaProfile); 1466 1467 if (isOnHold()) { 1468 if (DBG) { 1469 logi("update :: call is on hold"); 1470 } 1471 throw new ImsException("Not in a call to update call", 1472 ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE); 1473 } 1474 1475 synchronized(mLockObj) { 1476 if (mUpdateRequest != UPDATE_NONE) { 1477 if (DBG) { 1478 logi("update :: update is in progress; request=" + 1479 updateRequestToString(mUpdateRequest)); 1480 } 1481 throw new ImsException("Call update is in progress", 1482 ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE); 1483 } 1484 1485 if (mSession == null) { 1486 loge("update :: "); 1487 throw new ImsException("No call session", 1488 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 1489 } 1490 1491 mSession.update(callType, mediaProfile); 1492 mUpdateRequest = UPDATE_UNSPECIFIED; 1493 } 1494 } 1495 1496 /** 1497 * Extends this call (1-to-1 call) to the conference call 1498 * inviting the specified participants to. 1499 * 1500 */ 1501 public void extendToConference(String[] participants) throws ImsException { 1502 logi("extendToConference ::"); 1503 1504 if (isOnHold()) { 1505 if (DBG) { 1506 logi("extendToConference :: call is on hold"); 1507 } 1508 throw new ImsException("Not in a call to extend a call to conference", 1509 ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE); 1510 } 1511 1512 synchronized(mLockObj) { 1513 if (mUpdateRequest != UPDATE_NONE) { 1514 if (CONF_DBG) { 1515 logi("extendToConference :: update is in progress; request=" + 1516 updateRequestToString(mUpdateRequest)); 1517 } 1518 throw new ImsException("Call update is in progress", 1519 ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE); 1520 } 1521 1522 if (mSession == null) { 1523 loge("extendToConference :: "); 1524 throw new ImsException("No call session", 1525 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 1526 } 1527 1528 mSession.extendToConference(participants); 1529 mUpdateRequest = UPDATE_EXTEND_TO_CONFERENCE; 1530 } 1531 } 1532 1533 /** 1534 * Requests the conference server to invite an additional participants to the conference. 1535 * 1536 */ 1537 public void inviteParticipants(String[] participants) throws ImsException { 1538 logi("inviteParticipants ::"); 1539 1540 synchronized(mLockObj) { 1541 if (mSession == null) { 1542 loge("inviteParticipants :: "); 1543 throw new ImsException("No call session", 1544 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 1545 } 1546 1547 mSession.inviteParticipants(participants); 1548 } 1549 } 1550 1551 /** 1552 * Requests the conference server to remove the specified participants from the conference. 1553 * 1554 */ 1555 public void removeParticipants(String[] participants) throws ImsException { 1556 logi("removeParticipants :: session=" + mSession); 1557 synchronized(mLockObj) { 1558 if (mSession == null) { 1559 loge("removeParticipants :: "); 1560 throw new ImsException("No call session", 1561 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 1562 } 1563 1564 mSession.removeParticipants(participants); 1565 1566 } 1567 } 1568 1569 /** 1570 * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2833</a>, 1571 * event 0 ~ 9 maps to decimal value 0 ~ 9, '*' to 10, '#' to 11, event 'A' ~ 'D' to 12 ~ 15, 1572 * and event flash to 16. Currently, event flash is not supported. 1573 * 1574 * @param c that represents the DTMF to send. '0' ~ '9', 'A' ~ 'D', '*', '#' are valid inputs. 1575 * @param result the result message to send when done. 1576 */ 1577 public void sendDtmf(char c, Message result) { 1578 logi("sendDtmf :: code=" + c); 1579 1580 synchronized(mLockObj) { 1581 if (mSession != null) { 1582 mSession.sendDtmf(c, result); 1583 } 1584 } 1585 } 1586 1587 /** 1588 * Start a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2833</a>, 1589 * event 0 ~ 9 maps to decimal value 0 ~ 9, '*' to 10, '#' to 11, event 'A' ~ 'D' to 12 ~ 15, 1590 * and event flash to 16. Currently, event flash is not supported. 1591 * 1592 * @param c that represents the DTMF to send. '0' ~ '9', 'A' ~ 'D', '*', '#' are valid inputs. 1593 */ 1594 public void startDtmf(char c) { 1595 logi("startDtmf :: code=" + c); 1596 1597 synchronized(mLockObj) { 1598 if (mSession != null) { 1599 mSession.startDtmf(c); 1600 } 1601 } 1602 } 1603 1604 /** 1605 * Stop a DTMF code. 1606 */ 1607 public void stopDtmf() { 1608 logi("stopDtmf :: "); 1609 1610 synchronized(mLockObj) { 1611 if (mSession != null) { 1612 mSession.stopDtmf(); 1613 } 1614 } 1615 } 1616 1617 /** 1618 * Sends an USSD message. 1619 * 1620 * @param ussdMessage USSD message to send 1621 */ 1622 public void sendUssd(String ussdMessage) throws ImsException { 1623 logi("sendUssd :: ussdMessage=" + ussdMessage); 1624 1625 synchronized(mLockObj) { 1626 if (mSession == null) { 1627 loge("sendUssd :: "); 1628 throw new ImsException("No call session", 1629 ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED); 1630 } 1631 1632 mSession.sendUssd(ussdMessage); 1633 } 1634 } 1635 1636 public void sendRttMessage(String rttMessage) { 1637 synchronized(mLockObj) { 1638 if (mSession == null) { 1639 loge("sendRttMessage::no session"); 1640 } 1641 if (!mCallProfile.mMediaProfile.isRttCall()) { 1642 logi("sendRttMessage::Not an rtt call, ignoring"); 1643 return; 1644 } 1645 mSession.sendRttMessage(rttMessage); 1646 } 1647 } 1648 1649 /** 1650 * Sends a user-requested RTT upgrade request. 1651 */ 1652 public void sendRttModifyRequest() { 1653 logi("sendRttModifyRequest"); 1654 1655 synchronized(mLockObj) { 1656 if (mSession == null) { 1657 loge("sendRttModifyRequest::no session"); 1658 } 1659 if (mCallProfile.mMediaProfile.isRttCall()) { 1660 logi("sendRttModifyRequest::Already RTT call, ignoring."); 1661 return; 1662 } 1663 // Make a copy of the current ImsCallProfile and modify it to enable RTT 1664 Parcel p = Parcel.obtain(); 1665 mCallProfile.writeToParcel(p, 0); 1666 p.setDataPosition(0); 1667 ImsCallProfile requestedProfile = new ImsCallProfile(p); 1668 requestedProfile.mMediaProfile.setRttMode(ImsStreamMediaProfile.RTT_MODE_FULL); 1669 1670 mSession.sendRttModifyRequest(requestedProfile); 1671 } 1672 } 1673 1674 /** 1675 * Sends the user's response to a remotely-issued RTT upgrade request 1676 * 1677 * @param textStream A valid {@link Connection.RttTextStream} if the user 1678 * accepts, {@code null} if not. 1679 */ 1680 public void sendRttModifyResponse(boolean status) { 1681 logi("sendRttModifyResponse"); 1682 1683 synchronized(mLockObj) { 1684 if (mSession == null) { 1685 loge("sendRttModifyResponse::no session"); 1686 } 1687 if (mCallProfile.mMediaProfile.isRttCall()) { 1688 logi("sendRttModifyResponse::Already RTT call, ignoring."); 1689 return; 1690 } 1691 mSession.sendRttModifyResponse(status); 1692 } 1693 } 1694 1695 public void setAnswerWithRtt() { 1696 mAnswerWithRtt = true; 1697 } 1698 1699 private void clear(ImsReasonInfo lastReasonInfo) { 1700 mInCall = false; 1701 mHold = false; 1702 mUpdateRequest = UPDATE_NONE; 1703 mLastReasonInfo = lastReasonInfo; 1704 } 1705 1706 /** 1707 * Creates an IMS call session listener. 1708 */ 1709 private ImsCallSession.Listener createCallSessionListener() { 1710 mImsCallSessionListenerProxy = new ImsCallSessionListenerProxy(); 1711 return mImsCallSessionListenerProxy; 1712 } 1713 1714 /** 1715 * @return the current ImsCallSessionListenerProxy. NOTE: ONLY FOR USE WITH TESTING. 1716 */ 1717 @VisibleForTesting 1718 public ImsCallSessionListenerProxy getImsCallSessionListenerProxy() { 1719 return mImsCallSessionListenerProxy; 1720 } 1721 1722 private ImsCall createNewCall(ImsCallSession session, ImsCallProfile profile) { 1723 ImsCall call = new ImsCall(mContext, profile); 1724 1725 try { 1726 call.attachSession(session); 1727 } catch (ImsException e) { 1728 if (call != null) { 1729 call.close(); 1730 call = null; 1731 } 1732 } 1733 1734 // Do additional operations... 1735 1736 return call; 1737 } 1738 1739 private ImsStreamMediaProfile createHoldMediaProfile() { 1740 ImsStreamMediaProfile mediaProfile = new ImsStreamMediaProfile(); 1741 1742 if (mCallProfile == null) { 1743 return mediaProfile; 1744 } 1745 1746 mediaProfile.mAudioQuality = mCallProfile.mMediaProfile.mAudioQuality; 1747 mediaProfile.mVideoQuality = mCallProfile.mMediaProfile.mVideoQuality; 1748 mediaProfile.mAudioDirection = ImsStreamMediaProfile.DIRECTION_SEND; 1749 1750 if (mediaProfile.mVideoQuality != ImsStreamMediaProfile.VIDEO_QUALITY_NONE) { 1751 mediaProfile.mVideoDirection = ImsStreamMediaProfile.DIRECTION_SEND; 1752 } 1753 1754 return mediaProfile; 1755 } 1756 1757 private ImsStreamMediaProfile createResumeMediaProfile() { 1758 ImsStreamMediaProfile mediaProfile = new ImsStreamMediaProfile(); 1759 1760 if (mCallProfile == null) { 1761 return mediaProfile; 1762 } 1763 1764 mediaProfile.mAudioQuality = mCallProfile.mMediaProfile.mAudioQuality; 1765 mediaProfile.mVideoQuality = mCallProfile.mMediaProfile.mVideoQuality; 1766 mediaProfile.mAudioDirection = ImsStreamMediaProfile.DIRECTION_SEND_RECEIVE; 1767 1768 if (mediaProfile.mVideoQuality != ImsStreamMediaProfile.VIDEO_QUALITY_NONE) { 1769 mediaProfile.mVideoDirection = ImsStreamMediaProfile.DIRECTION_SEND_RECEIVE; 1770 } 1771 1772 return mediaProfile; 1773 } 1774 1775 private void enforceConversationMode() { 1776 if (mInCall) { 1777 mHold = false; 1778 mUpdateRequest = UPDATE_NONE; 1779 } 1780 } 1781 1782 private void mergeInternal() { 1783 if (CONF_DBG) { 1784 logi("mergeInternal :: "); 1785 } 1786 1787 mSession.merge(); 1788 mUpdateRequest = UPDATE_MERGE; 1789 } 1790 1791 private void notifyConferenceSessionTerminated(ImsReasonInfo reasonInfo) { 1792 ImsCall.Listener listener = mListener; 1793 clear(reasonInfo); 1794 1795 if (listener != null) { 1796 try { 1797 listener.onCallTerminated(this, reasonInfo); 1798 } catch (Throwable t) { 1799 loge("notifyConferenceSessionTerminated :: ", t); 1800 } 1801 } 1802 } 1803 1804 private void notifyConferenceStateUpdated(ImsConferenceState state) { 1805 if (state == null || state.mParticipants == null) { 1806 return; 1807 } 1808 1809 Set<Entry<String, Bundle>> participants = state.mParticipants.entrySet(); 1810 1811 if (participants == null) { 1812 return; 1813 } 1814 1815 Iterator<Entry<String, Bundle>> iterator = participants.iterator(); 1816 mConferenceParticipants = new ArrayList<>(participants.size()); 1817 while (iterator.hasNext()) { 1818 Entry<String, Bundle> entry = iterator.next(); 1819 1820 String key = entry.getKey(); 1821 Bundle confInfo = entry.getValue(); 1822 String status = confInfo.getString(ImsConferenceState.STATUS); 1823 String user = confInfo.getString(ImsConferenceState.USER); 1824 String displayName = confInfo.getString(ImsConferenceState.DISPLAY_TEXT); 1825 String endpoint = confInfo.getString(ImsConferenceState.ENDPOINT); 1826 1827 if (CONF_DBG) { 1828 logi("notifyConferenceStateUpdated :: key=" + Rlog.pii(TAG, key) + 1829 ", status=" + status + 1830 ", user=" + Rlog.pii(TAG, user) + 1831 ", displayName= " + Rlog.pii(TAG, displayName) + 1832 ", endpoint=" + endpoint); 1833 } 1834 1835 Uri handle = Uri.parse(user); 1836 if (endpoint == null) { 1837 endpoint = ""; 1838 } 1839 Uri endpointUri = Uri.parse(endpoint); 1840 int connectionState = ImsConferenceState.getConnectionStateForStatus(status); 1841 1842 if (connectionState != Connection.STATE_DISCONNECTED) { 1843 ConferenceParticipant conferenceParticipant = new ConferenceParticipant(handle, 1844 displayName, endpointUri, connectionState); 1845 mConferenceParticipants.add(conferenceParticipant); 1846 } 1847 } 1848 1849 if (mConferenceParticipants != null && mListener != null) { 1850 try { 1851 mListener.onConferenceParticipantsStateChanged(this, mConferenceParticipants); 1852 } catch (Throwable t) { 1853 loge("notifyConferenceStateUpdated :: ", t); 1854 } 1855 } 1856 } 1857 1858 /** 1859 * Perform all cleanup and notification around the termination of a session. 1860 * Note that there are 2 distinct modes of operation. The first is when 1861 * we receive a session termination on the primary session when we are 1862 * in the processing of merging. The second is when we are not merging anything 1863 * and the call is terminated. 1864 * 1865 * @param reasonInfo The reason for the session termination 1866 */ 1867 private void processCallTerminated(ImsReasonInfo reasonInfo) { 1868 logi("processCallTerminated :: reason=" + reasonInfo + " userInitiated = " + 1869 mTerminationRequestPending); 1870 1871 ImsCall.Listener listener = null; 1872 synchronized(ImsCall.this) { 1873 // If we are in the midst of establishing a conference, we will bury the termination 1874 // until the merge has completed. If necessary we can surface the termination at 1875 // this point. 1876 // We will also NOT bury the termination if a termination was initiated locally. 1877 if (isCallSessionMergePending() && !mTerminationRequestPending) { 1878 // Since we are in the process of a merge, this trigger means something 1879 // else because it is probably due to the merge happening vs. the 1880 // session is really terminated. Let's flag this and revisit if 1881 // the merge() ends up failing because we will need to take action on the 1882 // mSession in that case since the termination was not due to the merge 1883 // succeeding. 1884 if (CONF_DBG) { 1885 logi("processCallTerminated :: burying termination during ongoing merge."); 1886 } 1887 mSessionEndDuringMerge = true; 1888 mSessionEndDuringMergeReasonInfo = reasonInfo; 1889 return; 1890 } 1891 1892 // If we are terminating the conference call, notify using conference listeners. 1893 if (isMultiparty()) { 1894 notifyConferenceSessionTerminated(reasonInfo); 1895 return; 1896 } else { 1897 listener = mListener; 1898 clear(reasonInfo); 1899 } 1900 } 1901 1902 if (listener != null) { 1903 try { 1904 listener.onCallTerminated(ImsCall.this, reasonInfo); 1905 } catch (Throwable t) { 1906 loge("processCallTerminated :: ", t); 1907 } 1908 } 1909 } 1910 1911 /** 1912 * This function determines if the ImsCallSession is our actual ImsCallSession or if is 1913 * the transient session used in the process of creating a conference. This function should only 1914 * be called within callbacks that are not directly related to conference merging but might 1915 * potentially still be called on the transient ImsCallSession sent to us from 1916 * callSessionMergeStarted() when we don't really care. In those situations, we probably don't 1917 * want to take any action so we need to know that we can return early. 1918 * 1919 * @param session - The {@link ImsCallSession} that the function needs to analyze 1920 * @return true if this is the transient {@link ImsCallSession}, false otherwise. 1921 */ 1922 private boolean isTransientConferenceSession(ImsCallSession session) { 1923 if (session != null && session != mSession && session == mTransientConferenceSession) { 1924 return true; 1925 } 1926 return false; 1927 } 1928 1929 private void setTransientSessionAsPrimary(ImsCallSession transientSession) { 1930 synchronized (ImsCall.this) { 1931 mSession.setListener(null); 1932 mSession = transientSession; 1933 mSession.setListener(createCallSessionListener()); 1934 } 1935 } 1936 1937 private void markCallAsMerged(boolean playDisconnectTone) { 1938 if (!isSessionAlive(mSession)) { 1939 // If the peer is dead, let's not play a disconnect sound for it when we 1940 // unbury the termination callback. 1941 logi("markCallAsMerged"); 1942 setIsMerged(playDisconnectTone); 1943 mSessionEndDuringMerge = true; 1944 String reasonInfo; 1945 int reasonCode = ImsReasonInfo.CODE_UNSPECIFIED; 1946 if (playDisconnectTone) { 1947 reasonCode = ImsReasonInfo.CODE_USER_TERMINATED_BY_REMOTE; 1948 reasonInfo = "Call ended by network"; 1949 } else { 1950 reasonCode = ImsReasonInfo.CODE_LOCAL_ENDED_BY_CONFERENCE_MERGE; 1951 reasonInfo = "Call ended during conference merge process."; 1952 } 1953 mSessionEndDuringMergeReasonInfo = new ImsReasonInfo( 1954 reasonCode, 0, reasonInfo); 1955 } 1956 } 1957 1958 /** 1959 * Checks if the merge was requested by foreground conference call 1960 * 1961 * @return true if the merge was requested by foreground conference call 1962 */ 1963 public boolean isMergeRequestedByConf() { 1964 synchronized(mLockObj) { 1965 return mMergeRequestedByConference; 1966 } 1967 } 1968 1969 /** 1970 * Resets the flag which indicates merge request was sent by 1971 * foreground conference call 1972 */ 1973 public void resetIsMergeRequestedByConf(boolean value) { 1974 synchronized(mLockObj) { 1975 mMergeRequestedByConference = value; 1976 } 1977 } 1978 1979 /** 1980 * Returns current ImsCallSession 1981 * 1982 * @return current session 1983 */ 1984 public ImsCallSession getSession() { 1985 synchronized(mLockObj) { 1986 return mSession; 1987 } 1988 } 1989 1990 /** 1991 * We have detected that a initial conference call has been fully configured. The internal 1992 * state of both {@code ImsCall} objects need to be cleaned up to reflect the new state. 1993 * This function should only be called in the context of the merge host to simplify logic 1994 * 1995 */ 1996 private void processMergeComplete() { 1997 logi("processMergeComplete :: "); 1998 1999 // The logic simplifies if we can assume that this function is only called on 2000 // the merge host. 2001 if (!isMergeHost()) { 2002 loge("processMergeComplete :: We are not the merge host!"); 2003 return; 2004 } 2005 2006 ImsCall.Listener listener; 2007 boolean swapRequired = false; 2008 2009 ImsCall finalHostCall; 2010 ImsCall finalPeerCall; 2011 2012 synchronized(ImsCall.this) { 2013 if (isMultiparty()) { 2014 setIsMerged(false); 2015 // if case handles Case 4 explained in callSessionMergeComplete 2016 // otherwise it is case 5 2017 if (!mMergeRequestedByConference) { 2018 // single call in fg, conference call in bg. 2019 // Finally conf call becomes active after conference 2020 this.mHold = false; 2021 swapRequired = true; 2022 } 2023 mMergePeer.markCallAsMerged(false); 2024 finalHostCall = this; 2025 finalPeerCall = mMergePeer; 2026 } else { 2027 // If we are here, we are not trying to merge a new call into an existing 2028 // conference. That means that there is a transient session on the merge 2029 // host that represents the future conference once all the parties 2030 // have been added to it. So make sure that it exists or else something 2031 // very wrong is going on. 2032 if (mTransientConferenceSession == null) { 2033 loge("processMergeComplete :: No transient session!"); 2034 return; 2035 } 2036 if (mMergePeer == null) { 2037 loge("processMergeComplete :: No merge peer!"); 2038 return; 2039 } 2040 2041 // Since we are the host, we have the transient session attached to us. Let's detach 2042 // it and figure out where we need to set it for the final conference configuration. 2043 ImsCallSession transientConferenceSession = mTransientConferenceSession; 2044 mTransientConferenceSession = null; 2045 2046 // Clear the listener for this transient session, we'll create a new listener 2047 // when it is attached to the final ImsCall that it should live on. 2048 transientConferenceSession.setListener(null); 2049 2050 // Determine which call the transient session should be moved to. If the current 2051 // call session is still alive and the merge peer's session is not, we have a 2052 // situation where the current call failed to merge into the conference but the 2053 // merge peer did merge in to the conference. In this type of scenario the current 2054 // call will continue as a single party call, yet the background call will become 2055 // the conference. 2056 2057 // handles Case 3 explained in callSessionMergeComplete 2058 if (isSessionAlive(mSession) && !isSessionAlive(mMergePeer.getCallSession())) { 2059 // I'm the host but we are moving the transient session to the peer since its 2060 // session was disconnected and my session is still alive. This signifies that 2061 // their session was properly added to the conference but mine was not because 2062 // it is probably in the held state as opposed to part of the final conference. 2063 // In this case, we need to set isMerged to false on both calls so the 2064 // disconnect sound is called when either call disconnects. 2065 // Note that this case is only valid if this is an initial conference being 2066 // brought up. 2067 mMergePeer.mHold = false; 2068 this.mHold = true; 2069 if (mConferenceParticipants != null && !mConferenceParticipants.isEmpty()) { 2070 mMergePeer.mConferenceParticipants = mConferenceParticipants; 2071 } 2072 // At this point both host & peer will have participant information. 2073 // Peer will transition to host & the participant information 2074 // from that will be used 2075 // HostCall that failed to merge will remain as a single call with 2076 // mConferenceParticipants, which should not be used. 2077 // Expectation is that if this call becomes part of a conference call in future, 2078 // mConferenceParticipants will be overriten with new CEP that is received. 2079 finalHostCall = mMergePeer; 2080 finalPeerCall = this; 2081 swapRequired = true; 2082 setIsMerged(false); 2083 mMergePeer.setIsMerged(false); 2084 if (CONF_DBG) { 2085 logi("processMergeComplete :: transient will transfer to merge peer"); 2086 } 2087 } else if (!isSessionAlive(mSession) && 2088 isSessionAlive(mMergePeer.getCallSession())) { 2089 // Handles case 2 explained in callSessionMergeComplete 2090 // The transient session stays with us and the disconnect sound should be played 2091 // when the merge peer eventually disconnects since it was not actually added to 2092 // the conference and is probably sitting in the held state. 2093 finalHostCall = this; 2094 finalPeerCall = mMergePeer; 2095 swapRequired = false; 2096 setIsMerged(false); 2097 mMergePeer.setIsMerged(false); // Play the disconnect sound 2098 if (CONF_DBG) { 2099 logi("processMergeComplete :: transient will stay with the merge host"); 2100 } 2101 } else { 2102 // Handles case 1 explained in callSessionMergeComplete 2103 // The transient session stays with us and the disconnect sound should not be 2104 // played when we ripple up the disconnect for the merge peer because it was 2105 // only disconnected to be added to the conference. 2106 finalHostCall = this; 2107 finalPeerCall = mMergePeer; 2108 mMergePeer.markCallAsMerged(false); 2109 swapRequired = false; 2110 setIsMerged(false); 2111 mMergePeer.setIsMerged(true); 2112 if (CONF_DBG) { 2113 logi("processMergeComplete :: transient will stay with us (I'm the host)."); 2114 } 2115 } 2116 2117 if (CONF_DBG) { 2118 logi("processMergeComplete :: call=" + finalHostCall + " is the final host"); 2119 } 2120 2121 // Add the transient session to the ImsCall that ended up being the host for the 2122 // conference. 2123 finalHostCall.setTransientSessionAsPrimary(transientConferenceSession); 2124 } 2125 2126 listener = finalHostCall.mListener; 2127 2128 updateCallProfile(finalPeerCall); 2129 updateCallProfile(finalHostCall); 2130 2131 // Clear all the merge related flags. 2132 clearMergeInfo(); 2133 2134 // For the final peer...let's bubble up any possible disconnects that we had 2135 // during the merge process 2136 finalPeerCall.notifySessionTerminatedDuringMerge(); 2137 // For the final host, let's just bury the disconnects that we my have received 2138 // during the merge process since we are now the host of the conference call. 2139 finalHostCall.clearSessionTerminationFlags(); 2140 2141 // Keep track of the fact that merge host is the origin of a conference call in 2142 // progress. This is important so that we can later determine if a multiparty ImsCall 2143 // is multiparty because it was the origin of a conference call, or because it is a 2144 // member of a conference on another device. 2145 finalHostCall.mIsConferenceHost = true; 2146 } 2147 if (listener != null) { 2148 try { 2149 // finalPeerCall will have the participant that was not merged and 2150 // it will be held state 2151 // if peer was merged successfully, finalPeerCall will be null 2152 listener.onCallMerged(finalHostCall, finalPeerCall, swapRequired); 2153 } catch (Throwable t) { 2154 loge("processMergeComplete :: ", t); 2155 } 2156 if (mConferenceParticipants != null && !mConferenceParticipants.isEmpty()) { 2157 try { 2158 listener.onConferenceParticipantsStateChanged(finalHostCall, 2159 mConferenceParticipants); 2160 } catch (Throwable t) { 2161 loge("processMergeComplete :: ", t); 2162 } 2163 } 2164 } 2165 return; 2166 } 2167 2168 private static void updateCallProfile(ImsCall call) { 2169 if (call != null) { 2170 call.updateCallProfile(); 2171 } 2172 } 2173 2174 private void updateCallProfile() { 2175 synchronized (mLockObj) { 2176 if (mSession != null) { 2177 setCallProfile(mSession.getCallProfile()); 2178 } 2179 } 2180 } 2181 2182 /** 2183 * Handles the case where the session has ended during a merge by reporting the termination 2184 * reason to listeners. 2185 */ 2186 private void notifySessionTerminatedDuringMerge() { 2187 ImsCall.Listener listener; 2188 boolean notifyFailure = false; 2189 ImsReasonInfo notifyFailureReasonInfo = null; 2190 2191 synchronized(ImsCall.this) { 2192 listener = mListener; 2193 if (mSessionEndDuringMerge) { 2194 // Set some local variables that will send out a notification about a 2195 // previously buried termination callback for our primary session now that 2196 // we know that this is not due to the conference call merging successfully. 2197 if (CONF_DBG) { 2198 logi("notifySessionTerminatedDuringMerge ::reporting terminate during merge"); 2199 } 2200 notifyFailure = true; 2201 notifyFailureReasonInfo = mSessionEndDuringMergeReasonInfo; 2202 } 2203 clearSessionTerminationFlags(); 2204 } 2205 2206 if (listener != null && notifyFailure) { 2207 try { 2208 processCallTerminated(notifyFailureReasonInfo); 2209 } catch (Throwable t) { 2210 loge("notifySessionTerminatedDuringMerge :: ", t); 2211 } 2212 } 2213 } 2214 2215 private void clearSessionTerminationFlags() { 2216 mSessionEndDuringMerge = false; 2217 mSessionEndDuringMergeReasonInfo = null; 2218 } 2219 2220 /** 2221 * We received a callback from ImsCallSession that a merge failed. Clean up all 2222 * internal state to represent this state change. The calling function is a callback 2223 * and should have been called on the session that was in the foreground 2224 * when merge() was originally called. It is assumed that this function will be called 2225 * on the merge host. 2226 * 2227 * @param reasonInfo The {@link ImsReasonInfo} why the merge failed. 2228 */ 2229 private void processMergeFailed(ImsReasonInfo reasonInfo) { 2230 logi("processMergeFailed :: reason=" + reasonInfo); 2231 2232 ImsCall.Listener listener; 2233 synchronized(ImsCall.this) { 2234 // The logic simplifies if we can assume that this function is only called on 2235 // the merge host. 2236 if (!isMergeHost()) { 2237 loge("processMergeFailed :: We are not the merge host!"); 2238 return; 2239 } 2240 2241 // Try to clean up the transient session if it exists. 2242 if (mTransientConferenceSession != null) { 2243 mTransientConferenceSession.setListener(null); 2244 mTransientConferenceSession = null; 2245 } 2246 2247 listener = mListener; 2248 2249 // Ensure the calls being conferenced into the conference has isMerged = false. 2250 // Ensure any terminations are surfaced from this session. 2251 markCallAsMerged(true); 2252 setCallSessionMergePending(false); 2253 notifySessionTerminatedDuringMerge(); 2254 2255 // Perform the same cleanup on the merge peer if it exists. 2256 if (mMergePeer != null) { 2257 mMergePeer.markCallAsMerged(true); 2258 mMergePeer.setCallSessionMergePending(false); 2259 mMergePeer.notifySessionTerminatedDuringMerge(); 2260 } else { 2261 loge("processMergeFailed :: No merge peer!"); 2262 } 2263 2264 // Clear all the various flags around coordinating this merge. 2265 clearMergeInfo(); 2266 } 2267 if (listener != null) { 2268 try { 2269 listener.onCallMergeFailed(ImsCall.this, reasonInfo); 2270 } catch (Throwable t) { 2271 loge("processMergeFailed :: ", t); 2272 } 2273 } 2274 2275 return; 2276 } 2277 2278 @VisibleForTesting 2279 public class ImsCallSessionListenerProxy extends ImsCallSession.Listener { 2280 @Override 2281 public void callSessionProgressing(ImsCallSession session, ImsStreamMediaProfile profile) { 2282 logi("callSessionProgressing :: session=" + session + " profile=" + profile); 2283 2284 if (isTransientConferenceSession(session)) { 2285 // If it is a transient (conference) session, there is no action for this signal. 2286 logi("callSessionProgressing :: not supported for transient conference session=" + 2287 session); 2288 return; 2289 } 2290 2291 ImsCall.Listener listener; 2292 2293 synchronized(ImsCall.this) { 2294 listener = mListener; 2295 mCallProfile.mMediaProfile.copyFrom(profile); 2296 } 2297 2298 if (listener != null) { 2299 try { 2300 listener.onCallProgressing(ImsCall.this); 2301 } catch (Throwable t) { 2302 loge("callSessionProgressing :: ", t); 2303 } 2304 } 2305 } 2306 2307 @Override 2308 public void callSessionStarted(ImsCallSession session, ImsCallProfile profile) { 2309 logi("callSessionStarted :: session=" + session + " profile=" + profile); 2310 2311 if (!isTransientConferenceSession(session)) { 2312 // In the case that we are in the middle of a merge (either host or peer), we have 2313 // closure as far as this call's primary session is concerned. If we are not 2314 // merging...its a NOOP. 2315 setCallSessionMergePending(false); 2316 } else { 2317 logi("callSessionStarted :: on transient session=" + session); 2318 return; 2319 } 2320 2321 if (isTransientConferenceSession(session)) { 2322 // No further processing is needed if this is the transient session. 2323 return; 2324 } 2325 2326 ImsCall.Listener listener; 2327 2328 synchronized(ImsCall.this) { 2329 listener = mListener; 2330 setCallProfile(profile); 2331 } 2332 2333 if (listener != null) { 2334 try { 2335 listener.onCallStarted(ImsCall.this); 2336 } catch (Throwable t) { 2337 loge("callSessionStarted :: ", t); 2338 } 2339 } 2340 } 2341 2342 @Override 2343 public void callSessionStartFailed(ImsCallSession session, ImsReasonInfo reasonInfo) { 2344 loge("callSessionStartFailed :: session=" + session + " reasonInfo=" + reasonInfo); 2345 2346 if (isTransientConferenceSession(session)) { 2347 // We should not get this callback for a transient session. 2348 logi("callSessionStartFailed :: not supported for transient conference session=" + 2349 session); 2350 return; 2351 } 2352 2353 ImsCall.Listener listener; 2354 2355 synchronized(ImsCall.this) { 2356 listener = mListener; 2357 mLastReasonInfo = reasonInfo; 2358 } 2359 2360 if (listener != null) { 2361 try { 2362 listener.onCallStartFailed(ImsCall.this, reasonInfo); 2363 } catch (Throwable t) { 2364 loge("callSessionStarted :: ", t); 2365 } 2366 } 2367 } 2368 2369 @Override 2370 public void callSessionTerminated(ImsCallSession session, ImsReasonInfo reasonInfo) { 2371 logi("callSessionTerminated :: session=" + session + " reasonInfo=" + reasonInfo); 2372 2373 if (isTransientConferenceSession(session)) { 2374 logi("callSessionTerminated :: on transient session=" + session); 2375 // This is bad, it should be treated much a callSessionMergeFailed since the 2376 // transient session only exists when in the process of a merge and the 2377 // termination of this session is effectively the end of the merge. 2378 processMergeFailed(reasonInfo); 2379 return; 2380 } 2381 2382 if (mOverrideReason != ImsReasonInfo.CODE_UNSPECIFIED) { 2383 logi("callSessionTerminated :: overrideReasonInfo=" + mOverrideReason); 2384 reasonInfo = new ImsReasonInfo(mOverrideReason, reasonInfo.getExtraCode(), 2385 reasonInfo.getExtraMessage()); 2386 } 2387 2388 // Process the termination first. If we are in the midst of establishing a conference 2389 // call, we may bury this callback until we are done. If there so no conference 2390 // call, the code after this function will be a NOOP. 2391 processCallTerminated(reasonInfo); 2392 2393 // If session has terminated, it is no longer pending merge. 2394 setCallSessionMergePending(false); 2395 2396 } 2397 2398 @Override 2399 public void callSessionHeld(ImsCallSession session, ImsCallProfile profile) { 2400 logi("callSessionHeld :: session=" + session + "profile=" + profile); 2401 ImsCall.Listener listener; 2402 2403 synchronized(ImsCall.this) { 2404 // If the session was held, it is no longer pending a merge -- this means it could 2405 // not be merged into the conference and was held instead. 2406 setCallSessionMergePending(false); 2407 2408 setCallProfile(profile); 2409 2410 if (mUpdateRequest == UPDATE_HOLD_MERGE) { 2411 // This hold request was made to set the stage for a merge. 2412 mergeInternal(); 2413 return; 2414 } 2415 2416 mUpdateRequest = UPDATE_NONE; 2417 listener = mListener; 2418 } 2419 2420 if (listener != null) { 2421 try { 2422 listener.onCallHeld(ImsCall.this); 2423 } catch (Throwable t) { 2424 loge("callSessionHeld :: ", t); 2425 } 2426 } 2427 } 2428 2429 @Override 2430 public void callSessionHoldFailed(ImsCallSession session, ImsReasonInfo reasonInfo) { 2431 loge("callSessionHoldFailed :: session" + session + "reasonInfo=" + reasonInfo); 2432 2433 if (isTransientConferenceSession(session)) { 2434 // We should not get this callback for a transient session. 2435 logi("callSessionHoldFailed :: not supported for transient conference session=" + 2436 session); 2437 return; 2438 } 2439 2440 logi("callSessionHoldFailed :: session=" + session + 2441 ", reasonInfo=" + reasonInfo); 2442 2443 synchronized (mLockObj) { 2444 mHold = false; 2445 } 2446 2447 boolean isHoldForMerge = false; 2448 ImsCall.Listener listener; 2449 2450 synchronized(ImsCall.this) { 2451 if (mUpdateRequest == UPDATE_HOLD_MERGE) { 2452 isHoldForMerge = true; 2453 } 2454 2455 mUpdateRequest = UPDATE_NONE; 2456 listener = mListener; 2457 } 2458 2459 if (listener != null) { 2460 try { 2461 listener.onCallHoldFailed(ImsCall.this, reasonInfo); 2462 } catch (Throwable t) { 2463 loge("callSessionHoldFailed :: ", t); 2464 } 2465 } 2466 } 2467 2468 /** 2469 * Indicates that an {@link ImsCallSession} has been remotely held. This can be due to the 2470 * remote party holding the current call, or swapping between calls. 2471 * @param session the session which was held. 2472 * @param profile the profile for the held call. 2473 */ 2474 @Override 2475 public void callSessionHoldReceived(ImsCallSession session, ImsCallProfile profile) { 2476 logi("callSessionHoldReceived :: session=" + session + "profile=" + profile); 2477 2478 if (isTransientConferenceSession(session)) { 2479 // We should not get this callback for a transient session. 2480 logi("callSessionHoldReceived :: not supported for transient conference session=" + 2481 session); 2482 return; 2483 } 2484 2485 ImsCall.Listener listener; 2486 2487 synchronized(ImsCall.this) { 2488 listener = mListener; 2489 setCallProfile(profile); 2490 } 2491 2492 if (listener != null) { 2493 try { 2494 listener.onCallHoldReceived(ImsCall.this); 2495 } catch (Throwable t) { 2496 loge("callSessionHoldReceived :: ", t); 2497 } 2498 } 2499 } 2500 2501 /** 2502 * Indicates that an {@link ImsCallSession} has been remotely resumed. This can be due to 2503 * the remote party un-holding the current call, or swapping back to this call. 2504 * @param session the session which was resumed. 2505 * @param profile the profile for the held call. 2506 */ 2507 @Override 2508 public void callSessionResumed(ImsCallSession session, ImsCallProfile profile) { 2509 logi("callSessionResumed :: session=" + session + "profile=" + profile); 2510 2511 if (isTransientConferenceSession(session)) { 2512 logi("callSessionResumed :: not supported for transient conference session=" + 2513 session); 2514 return; 2515 } 2516 2517 // If this call was pending a merge, it is not anymore. This is the case when we 2518 // are merging in a new call into an existing conference. 2519 setCallSessionMergePending(false); 2520 2521 // TOOD: When we are merging a new call into an existing conference we are waiting 2522 // for 2 triggers to let us know that the conference has been established, the first 2523 // is a termination for the new calls (since it is added to the conference) the second 2524 // would be a resume on the existing conference. If the resume comes first, then 2525 // we will make the onCallResumed() callback and its unclear how this will behave if 2526 // the termination has not come yet. 2527 2528 ImsCall.Listener listener; 2529 synchronized(ImsCall.this) { 2530 listener = mListener; 2531 setCallProfile(profile); 2532 mUpdateRequest = UPDATE_NONE; 2533 mHold = false; 2534 } 2535 2536 if (listener != null) { 2537 try { 2538 listener.onCallResumed(ImsCall.this); 2539 } catch (Throwable t) { 2540 loge("callSessionResumed :: ", t); 2541 } 2542 } 2543 } 2544 2545 @Override 2546 public void callSessionResumeFailed(ImsCallSession session, ImsReasonInfo reasonInfo) { 2547 loge("callSessionResumeFailed :: session=" + session + "reasonInfo=" + reasonInfo); 2548 2549 if (isTransientConferenceSession(session)) { 2550 logi("callSessionResumeFailed :: not supported for transient conference session=" + 2551 session); 2552 return; 2553 } 2554 2555 synchronized(mLockObj) { 2556 mHold = true; 2557 } 2558 2559 ImsCall.Listener listener; 2560 2561 synchronized(ImsCall.this) { 2562 listener = mListener; 2563 mUpdateRequest = UPDATE_NONE; 2564 } 2565 2566 if (listener != null) { 2567 try { 2568 listener.onCallResumeFailed(ImsCall.this, reasonInfo); 2569 } catch (Throwable t) { 2570 loge("callSessionResumeFailed :: ", t); 2571 } 2572 } 2573 } 2574 2575 @Override 2576 public void callSessionResumeReceived(ImsCallSession session, ImsCallProfile profile) { 2577 logi("callSessionResumeReceived :: session=" + session + "profile=" + profile); 2578 2579 if (isTransientConferenceSession(session)) { 2580 logi("callSessionResumeReceived :: not supported for transient conference session=" + 2581 session); 2582 return; 2583 } 2584 2585 ImsCall.Listener listener; 2586 2587 synchronized(ImsCall.this) { 2588 listener = mListener; 2589 setCallProfile(profile); 2590 } 2591 2592 if (listener != null) { 2593 try { 2594 listener.onCallResumeReceived(ImsCall.this); 2595 } catch (Throwable t) { 2596 loge("callSessionResumeReceived :: ", t); 2597 } 2598 } 2599 } 2600 2601 @Override 2602 public void callSessionMergeStarted(ImsCallSession session, 2603 ImsCallSession newSession, ImsCallProfile profile) { 2604 logi("callSessionMergeStarted :: session=" + session + " newSession=" + newSession + 2605 ", profile=" + profile); 2606 2607 return; 2608 } 2609 2610 /* 2611 * This method check if session exists as a session on the current 2612 * ImsCall or its counterpart if it is in the process of a conference 2613 */ 2614 private boolean doesCallSessionExistsInMerge(ImsCallSession cs) { 2615 String callId = cs.getCallId(); 2616 return ((isMergeHost() && Objects.equals(mMergePeer.mSession.getCallId(), callId)) || 2617 (isMergePeer() && Objects.equals(mMergeHost.mSession.getCallId(), callId)) || 2618 Objects.equals(mSession.getCallId(), callId)); 2619 } 2620 2621 /** 2622 * We received a callback from ImsCallSession that merge completed. 2623 * @param newSession - this session can have 2 values based on the below scenarios 2624 * 2625 * Conference Scenarios : 2626 * Case 1 - 3 way success case 2627 * Case 2 - 3 way success case but held call fails to merge 2628 * Case 3 - 3 way success case but active call fails to merge 2629 * case 4 - 4 way success case, where merge is initiated on the foreground single-party 2630 * call and the conference (mergeHost) is the background call. 2631 * case 5 - 4 way success case, where merge is initiated on the foreground conference 2632 * call (mergeHost) and the single party call is in the background. 2633 * 2634 * Conference Result: 2635 * session : new session after conference 2636 * newSession = new session for case 1, 2, 3. 2637 * Should be considered as mTransientConferencession 2638 * newSession = Active conference session for case 5 will be null 2639 * mergehost was foreground call 2640 * mTransientConferencession will be null 2641 * newSession = Active conference session for case 4 will be null 2642 * mergeHost was background call 2643 * mTransientConferencession will be null 2644 */ 2645 @Override 2646 public void callSessionMergeComplete(ImsCallSession newSession) { 2647 logi("callSessionMergeComplete :: newSession =" + newSession); 2648 if (!isMergeHost()) { 2649 // Handles case 4 2650 mMergeHost.processMergeComplete(); 2651 } else { 2652 // Handles case 1, 2, 3 2653 if (newSession != null) { 2654 mTransientConferenceSession = doesCallSessionExistsInMerge(newSession) ? 2655 null: newSession; 2656 } 2657 // Handles case 5 2658 processMergeComplete(); 2659 } 2660 } 2661 2662 @Override 2663 public void callSessionMergeFailed(ImsCallSession session, ImsReasonInfo reasonInfo) { 2664 loge("callSessionMergeFailed :: session=" + session + "reasonInfo=" + reasonInfo); 2665 2666 // Its possible that there could be threading issues with the other thread handling 2667 // the other call. This could affect our state. 2668 synchronized (ImsCall.this) { 2669 // Let's tell our parent ImsCall that the merge has failed and we need to clean 2670 // up any temporary, transient state. Note this only gets called for an initial 2671 // conference. If a merge into an existing conference fails, the two sessions will 2672 // just go back to their original state (ACTIVE or HELD). 2673 if (isMergeHost()) { 2674 processMergeFailed(reasonInfo); 2675 } else if (mMergeHost != null) { 2676 mMergeHost.processMergeFailed(reasonInfo); 2677 } else { 2678 loge("callSessionMergeFailed :: No merge host for this conference!"); 2679 } 2680 } 2681 } 2682 2683 @Override 2684 public void callSessionUpdated(ImsCallSession session, ImsCallProfile profile) { 2685 logi("callSessionUpdated :: session=" + session + " profile=" + profile); 2686 2687 if (isTransientConferenceSession(session)) { 2688 logi("callSessionUpdated :: not supported for transient conference session=" + 2689 session); 2690 return; 2691 } 2692 2693 ImsCall.Listener listener; 2694 2695 synchronized(ImsCall.this) { 2696 listener = mListener; 2697 setCallProfile(profile); 2698 } 2699 2700 if (listener != null) { 2701 try { 2702 listener.onCallUpdated(ImsCall.this); 2703 } catch (Throwable t) { 2704 loge("callSessionUpdated :: ", t); 2705 } 2706 } 2707 } 2708 2709 @Override 2710 public void callSessionUpdateFailed(ImsCallSession session, ImsReasonInfo reasonInfo) { 2711 loge("callSessionUpdateFailed :: session=" + session + " reasonInfo=" + reasonInfo); 2712 2713 if (isTransientConferenceSession(session)) { 2714 logi("callSessionUpdateFailed :: not supported for transient conference session=" + 2715 session); 2716 return; 2717 } 2718 2719 ImsCall.Listener listener; 2720 2721 synchronized(ImsCall.this) { 2722 listener = mListener; 2723 mUpdateRequest = UPDATE_NONE; 2724 } 2725 2726 if (listener != null) { 2727 try { 2728 listener.onCallUpdateFailed(ImsCall.this, reasonInfo); 2729 } catch (Throwable t) { 2730 loge("callSessionUpdateFailed :: ", t); 2731 } 2732 } 2733 } 2734 2735 @Override 2736 public void callSessionUpdateReceived(ImsCallSession session, ImsCallProfile profile) { 2737 logi("callSessionUpdateReceived :: session=" + session + " profile=" + profile); 2738 2739 if (isTransientConferenceSession(session)) { 2740 logi("callSessionUpdateReceived :: not supported for transient conference " + 2741 "session=" + session); 2742 return; 2743 } 2744 2745 ImsCall.Listener listener; 2746 2747 synchronized(ImsCall.this) { 2748 listener = mListener; 2749 mProposedCallProfile = profile; 2750 mUpdateRequest = UPDATE_UNSPECIFIED; 2751 } 2752 2753 if (listener != null) { 2754 try { 2755 listener.onCallUpdateReceived(ImsCall.this); 2756 } catch (Throwable t) { 2757 loge("callSessionUpdateReceived :: ", t); 2758 } 2759 } 2760 } 2761 2762 @Override 2763 public void callSessionConferenceExtended(ImsCallSession session, ImsCallSession newSession, 2764 ImsCallProfile profile) { 2765 logi("callSessionConferenceExtended :: session=" + session + " newSession=" + 2766 newSession + ", profile=" + profile); 2767 2768 if (isTransientConferenceSession(session)) { 2769 logi("callSessionConferenceExtended :: not supported for transient conference " + 2770 "session=" + session); 2771 return; 2772 } 2773 2774 ImsCall newCall = createNewCall(newSession, profile); 2775 2776 if (newCall == null) { 2777 callSessionConferenceExtendFailed(session, new ImsReasonInfo()); 2778 return; 2779 } 2780 2781 ImsCall.Listener listener; 2782 2783 synchronized(ImsCall.this) { 2784 listener = mListener; 2785 mUpdateRequest = UPDATE_NONE; 2786 } 2787 2788 if (listener != null) { 2789 try { 2790 listener.onCallConferenceExtended(ImsCall.this, newCall); 2791 } catch (Throwable t) { 2792 loge("callSessionConferenceExtended :: ", t); 2793 } 2794 } 2795 } 2796 2797 @Override 2798 public void callSessionConferenceExtendFailed(ImsCallSession session, 2799 ImsReasonInfo reasonInfo) { 2800 loge("callSessionConferenceExtendFailed :: reasonInfo=" + reasonInfo); 2801 2802 if (isTransientConferenceSession(session)) { 2803 logi("callSessionConferenceExtendFailed :: not supported for transient " + 2804 "conference session=" + session); 2805 return; 2806 } 2807 2808 ImsCall.Listener listener; 2809 2810 synchronized(ImsCall.this) { 2811 listener = mListener; 2812 mUpdateRequest = UPDATE_NONE; 2813 } 2814 2815 if (listener != null) { 2816 try { 2817 listener.onCallConferenceExtendFailed(ImsCall.this, reasonInfo); 2818 } catch (Throwable t) { 2819 loge("callSessionConferenceExtendFailed :: ", t); 2820 } 2821 } 2822 } 2823 2824 @Override 2825 public void callSessionConferenceExtendReceived(ImsCallSession session, 2826 ImsCallSession newSession, ImsCallProfile profile) { 2827 logi("callSessionConferenceExtendReceived :: newSession=" + newSession + 2828 ", profile=" + profile); 2829 2830 if (isTransientConferenceSession(session)) { 2831 logi("callSessionConferenceExtendReceived :: not supported for transient " + 2832 "conference session" + session); 2833 return; 2834 } 2835 2836 ImsCall newCall = createNewCall(newSession, profile); 2837 2838 if (newCall == null) { 2839 // Should all the calls be terminated...??? 2840 return; 2841 } 2842 2843 ImsCall.Listener listener; 2844 2845 synchronized(ImsCall.this) { 2846 listener = mListener; 2847 } 2848 2849 if (listener != null) { 2850 try { 2851 listener.onCallConferenceExtendReceived(ImsCall.this, newCall); 2852 } catch (Throwable t) { 2853 loge("callSessionConferenceExtendReceived :: ", t); 2854 } 2855 } 2856 } 2857 2858 @Override 2859 public void callSessionInviteParticipantsRequestDelivered(ImsCallSession session) { 2860 logi("callSessionInviteParticipantsRequestDelivered ::"); 2861 2862 if (isTransientConferenceSession(session)) { 2863 logi("callSessionInviteParticipantsRequestDelivered :: not supported for " + 2864 "conference session=" + session); 2865 return; 2866 } 2867 2868 ImsCall.Listener listener; 2869 2870 synchronized(ImsCall.this) { 2871 listener = mListener; 2872 } 2873 2874 if (listener != null) { 2875 try { 2876 listener.onCallInviteParticipantsRequestDelivered(ImsCall.this); 2877 } catch (Throwable t) { 2878 loge("callSessionInviteParticipantsRequestDelivered :: ", t); 2879 } 2880 } 2881 } 2882 2883 @Override 2884 public void callSessionInviteParticipantsRequestFailed(ImsCallSession session, 2885 ImsReasonInfo reasonInfo) { 2886 loge("callSessionInviteParticipantsRequestFailed :: reasonInfo=" + reasonInfo); 2887 2888 if (isTransientConferenceSession(session)) { 2889 logi("callSessionInviteParticipantsRequestFailed :: not supported for " + 2890 "conference session=" + session); 2891 return; 2892 } 2893 2894 ImsCall.Listener listener; 2895 2896 synchronized(ImsCall.this) { 2897 listener = mListener; 2898 } 2899 2900 if (listener != null) { 2901 try { 2902 listener.onCallInviteParticipantsRequestFailed(ImsCall.this, reasonInfo); 2903 } catch (Throwable t) { 2904 loge("callSessionInviteParticipantsRequestFailed :: ", t); 2905 } 2906 } 2907 } 2908 2909 @Override 2910 public void callSessionRemoveParticipantsRequestDelivered(ImsCallSession session) { 2911 logi("callSessionRemoveParticipantsRequestDelivered ::"); 2912 2913 if (isTransientConferenceSession(session)) { 2914 logi("callSessionRemoveParticipantsRequestDelivered :: not supported for " + 2915 "conference session=" + session); 2916 return; 2917 } 2918 2919 ImsCall.Listener listener; 2920 2921 synchronized(ImsCall.this) { 2922 listener = mListener; 2923 } 2924 2925 if (listener != null) { 2926 try { 2927 listener.onCallRemoveParticipantsRequestDelivered(ImsCall.this); 2928 } catch (Throwable t) { 2929 loge("callSessionRemoveParticipantsRequestDelivered :: ", t); 2930 } 2931 } 2932 } 2933 2934 @Override 2935 public void callSessionRemoveParticipantsRequestFailed(ImsCallSession session, 2936 ImsReasonInfo reasonInfo) { 2937 loge("callSessionRemoveParticipantsRequestFailed :: reasonInfo=" + reasonInfo); 2938 2939 if (isTransientConferenceSession(session)) { 2940 logi("callSessionRemoveParticipantsRequestFailed :: not supported for " + 2941 "conference session=" + session); 2942 return; 2943 } 2944 2945 ImsCall.Listener listener; 2946 2947 synchronized(ImsCall.this) { 2948 listener = mListener; 2949 } 2950 2951 if (listener != null) { 2952 try { 2953 listener.onCallRemoveParticipantsRequestFailed(ImsCall.this, reasonInfo); 2954 } catch (Throwable t) { 2955 loge("callSessionRemoveParticipantsRequestFailed :: ", t); 2956 } 2957 } 2958 } 2959 2960 @Override 2961 public void callSessionConferenceStateUpdated(ImsCallSession session, 2962 ImsConferenceState state) { 2963 logi("callSessionConferenceStateUpdated :: state=" + state); 2964 2965 conferenceStateUpdated(state); 2966 } 2967 2968 @Override 2969 public void callSessionUssdMessageReceived(ImsCallSession session, int mode, 2970 String ussdMessage) { 2971 logi("callSessionUssdMessageReceived :: mode=" + mode + ", ussdMessage=" + 2972 ussdMessage); 2973 2974 if (isTransientConferenceSession(session)) { 2975 logi("callSessionUssdMessageReceived :: not supported for transient " + 2976 "conference session=" + session); 2977 return; 2978 } 2979 2980 ImsCall.Listener listener; 2981 2982 synchronized(ImsCall.this) { 2983 listener = mListener; 2984 } 2985 2986 if (listener != null) { 2987 try { 2988 listener.onCallUssdMessageReceived(ImsCall.this, mode, ussdMessage); 2989 } catch (Throwable t) { 2990 loge("callSessionUssdMessageReceived :: ", t); 2991 } 2992 } 2993 } 2994 2995 @Override 2996 public void callSessionTtyModeReceived(ImsCallSession session, int mode) { 2997 logi("callSessionTtyModeReceived :: mode=" + mode); 2998 2999 ImsCall.Listener listener; 3000 3001 synchronized(ImsCall.this) { 3002 listener = mListener; 3003 } 3004 3005 if (listener != null) { 3006 try { 3007 listener.onCallSessionTtyModeReceived(ImsCall.this, mode); 3008 } catch (Throwable t) { 3009 loge("callSessionTtyModeReceived :: ", t); 3010 } 3011 } 3012 } 3013 3014 /** 3015 * Notifies of a change to the multiparty state for this {@code ImsCallSession}. 3016 * 3017 * @param session The call session. 3018 * @param isMultiParty {@code true} if the session became multiparty, {@code false} 3019 * otherwise. 3020 */ 3021 @Override 3022 public void callSessionMultipartyStateChanged(ImsCallSession session, 3023 boolean isMultiParty) { 3024 if (VDBG) { 3025 logi("callSessionMultipartyStateChanged isMultiParty: " + (isMultiParty ? "Y" 3026 : "N")); 3027 } 3028 3029 ImsCall.Listener listener; 3030 3031 synchronized(ImsCall.this) { 3032 listener = mListener; 3033 } 3034 3035 if (listener != null) { 3036 try { 3037 listener.onMultipartyStateChanged(ImsCall.this, isMultiParty); 3038 } catch (Throwable t) { 3039 loge("callSessionMultipartyStateChanged :: ", t); 3040 } 3041 } 3042 } 3043 3044 public void callSessionHandover(ImsCallSession session, int srcAccessTech, 3045 int targetAccessTech, ImsReasonInfo reasonInfo) { 3046 logi("callSessionHandover :: session=" + session + ", srcAccessTech=" + 3047 srcAccessTech + ", targetAccessTech=" + targetAccessTech + ", reasonInfo=" + 3048 reasonInfo); 3049 3050 ImsCall.Listener listener; 3051 3052 synchronized(ImsCall.this) { 3053 listener = mListener; 3054 } 3055 3056 if (listener != null) { 3057 try { 3058 listener.onCallHandover(ImsCall.this, srcAccessTech, targetAccessTech, 3059 reasonInfo); 3060 } catch (Throwable t) { 3061 loge("callSessionHandover :: ", t); 3062 } 3063 } 3064 } 3065 3066 @Override 3067 public void callSessionHandoverFailed(ImsCallSession session, int srcAccessTech, 3068 int targetAccessTech, ImsReasonInfo reasonInfo) { 3069 loge("callSessionHandoverFailed :: session=" + session + ", srcAccessTech=" + 3070 srcAccessTech + ", targetAccessTech=" + targetAccessTech + ", reasonInfo=" + 3071 reasonInfo); 3072 3073 ImsCall.Listener listener; 3074 3075 synchronized(ImsCall.this) { 3076 listener = mListener; 3077 } 3078 3079 if (listener != null) { 3080 try { 3081 listener.onCallHandoverFailed(ImsCall.this, srcAccessTech, targetAccessTech, 3082 reasonInfo); 3083 } catch (Throwable t) { 3084 loge("callSessionHandoverFailed :: ", t); 3085 } 3086 } 3087 } 3088 3089 @Override 3090 public void callSessionSuppServiceReceived(ImsCallSession session, 3091 ImsSuppServiceNotification suppServiceInfo ) { 3092 if (isTransientConferenceSession(session)) { 3093 logi("callSessionSuppServiceReceived :: not supported for transient conference" 3094 + " session=" + session); 3095 return; 3096 } 3097 3098 logi("callSessionSuppServiceReceived :: session=" + session + 3099 ", suppServiceInfo" + suppServiceInfo); 3100 3101 ImsCall.Listener listener; 3102 3103 synchronized(ImsCall.this) { 3104 listener = mListener; 3105 } 3106 3107 if (listener != null) { 3108 try { 3109 listener.onCallSuppServiceReceived(ImsCall.this, suppServiceInfo); 3110 } catch (Throwable t) { 3111 loge("callSessionSuppServiceReceived :: ", t); 3112 } 3113 } 3114 } 3115 3116 @Override 3117 public void callSessionRttModifyRequestReceived(ImsCallSession session, 3118 ImsCallProfile callProfile) { 3119 ImsCall.Listener listener; 3120 logi("callSessionRttModifyRequestReceived"); 3121 3122 synchronized(ImsCall.this) { 3123 listener = mListener; 3124 } 3125 3126 if (!callProfile.mMediaProfile.isRttCall()) { 3127 logi("callSessionRttModifyRequestReceived:: ignoring request, requested profile " + 3128 "is not RTT."); 3129 return; 3130 } 3131 3132 if (listener != null) { 3133 try { 3134 listener.onRttModifyRequestReceived(ImsCall.this); 3135 } catch (Throwable t) { 3136 loge("callSessionRttModifyRequestReceived:: ", t); 3137 } 3138 } 3139 } 3140 3141 @Override 3142 public void callSessionRttModifyResponseReceived(int status) { 3143 ImsCall.Listener listener; 3144 3145 logi("callSessionRttModifyResponseReceived: " + status); 3146 synchronized(ImsCall.this) { 3147 listener = mListener; 3148 } 3149 3150 if (listener != null) { 3151 try { 3152 listener.onRttModifyResponseReceived(ImsCall.this, status); 3153 } catch (Throwable t) { 3154 loge("callSessionRttModifyResponseReceived:: ", t); 3155 } 3156 } 3157 } 3158 3159 @Override 3160 public void callSessionRttMessageReceived(String rttMessage) { 3161 ImsCall.Listener listener; 3162 3163 synchronized(ImsCall.this) { 3164 listener = mListener; 3165 } 3166 3167 if (listener != null) { 3168 try { 3169 listener.onRttMessageReceived(ImsCall.this, rttMessage); 3170 } catch (Throwable t) { 3171 loge("callSessionRttModifyResponseReceived:: ", t); 3172 } 3173 } 3174 } 3175 } 3176 3177 /** 3178 * Report a new conference state to the current {@link ImsCall} and inform listeners of the 3179 * change. Marked as {@code VisibleForTesting} so that the 3180 * {@code com.android.internal.telephony.TelephonyTester} class can inject a test conference 3181 * event package into a regular ongoing IMS call. 3182 * 3183 * @param state The {@link ImsConferenceState}. 3184 */ 3185 @VisibleForTesting 3186 public void conferenceStateUpdated(ImsConferenceState state) { 3187 Listener listener; 3188 3189 synchronized(this) { 3190 notifyConferenceStateUpdated(state); 3191 listener = mListener; 3192 } 3193 3194 if (listener != null) { 3195 try { 3196 listener.onCallConferenceStateUpdated(this, state); 3197 } catch (Throwable t) { 3198 loge("callSessionConferenceStateUpdated :: ", t); 3199 } 3200 } 3201 } 3202 3203 /** 3204 * Provides a human-readable string representation of an update request. 3205 * 3206 * @param updateRequest The update request. 3207 * @return The string representation. 3208 */ 3209 private String updateRequestToString(int updateRequest) { 3210 switch (updateRequest) { 3211 case UPDATE_NONE: 3212 return "NONE"; 3213 case UPDATE_HOLD: 3214 return "HOLD"; 3215 case UPDATE_HOLD_MERGE: 3216 return "HOLD_MERGE"; 3217 case UPDATE_RESUME: 3218 return "RESUME"; 3219 case UPDATE_MERGE: 3220 return "MERGE"; 3221 case UPDATE_EXTEND_TO_CONFERENCE: 3222 return "EXTEND_TO_CONFERENCE"; 3223 case UPDATE_UNSPECIFIED: 3224 return "UNSPECIFIED"; 3225 default: 3226 return "UNKNOWN"; 3227 } 3228 } 3229 3230 /** 3231 * Clears the merge peer for this call, ensuring that the peer's connection to this call is also 3232 * severed at the same time. 3233 */ 3234 private void clearMergeInfo() { 3235 if (CONF_DBG) { 3236 logi("clearMergeInfo :: clearing all merge info"); 3237 } 3238 3239 // First clear out the merge partner then clear ourselves out. 3240 if (mMergeHost != null) { 3241 mMergeHost.mMergePeer = null; 3242 mMergeHost.mUpdateRequest = UPDATE_NONE; 3243 mMergeHost.mCallSessionMergePending = false; 3244 } 3245 if (mMergePeer != null) { 3246 mMergePeer.mMergeHost = null; 3247 mMergePeer.mUpdateRequest = UPDATE_NONE; 3248 mMergePeer.mCallSessionMergePending = false; 3249 } 3250 mMergeHost = null; 3251 mMergePeer = null; 3252 mUpdateRequest = UPDATE_NONE; 3253 mCallSessionMergePending = false; 3254 } 3255 3256 /** 3257 * Sets the merge peer for the current call. The merge peer is the background call that will be 3258 * merged into this call. On the merge peer, sets the merge host to be this call. 3259 * 3260 * @param mergePeer The peer call to be merged into this one. 3261 */ 3262 private void setMergePeer(ImsCall mergePeer) { 3263 mMergePeer = mergePeer; 3264 mMergeHost = null; 3265 3266 mergePeer.mMergeHost = ImsCall.this; 3267 mergePeer.mMergePeer = null; 3268 } 3269 3270 /** 3271 * Sets the merge hody for the current call. The merge host is the foreground call this call 3272 * will be merged into. On the merge host, sets the merge peer to be this call. 3273 * 3274 * @param mergeHost The merge host this call will be merged into. 3275 */ 3276 public void setMergeHost(ImsCall mergeHost) { 3277 mMergeHost = mergeHost; 3278 mMergePeer = null; 3279 3280 mergeHost.mMergeHost = null; 3281 mergeHost.mMergePeer = ImsCall.this; 3282 } 3283 3284 /** 3285 * Determines if the current call is in the process of merging with another call or conference. 3286 * 3287 * @return {@code true} if in the process of merging. 3288 */ 3289 private boolean isMerging() { 3290 return mMergePeer != null || mMergeHost != null; 3291 } 3292 3293 /** 3294 * Determines if the current call is the host of the merge. 3295 * 3296 * @return {@code true} if the call is the merge host. 3297 */ 3298 private boolean isMergeHost() { 3299 return mMergePeer != null && mMergeHost == null; 3300 } 3301 3302 /** 3303 * Determines if the current call is the peer of the merge. 3304 * 3305 * @return {@code true} if the call is the merge peer. 3306 */ 3307 private boolean isMergePeer() { 3308 return mMergePeer == null && mMergeHost != null; 3309 } 3310 3311 /** 3312 * Determines if the call session is pending merge into a conference or not. 3313 * 3314 * @return {@code true} if a merge into a conference is pending, {@code false} otherwise. 3315 */ 3316 public boolean isCallSessionMergePending() { 3317 return mCallSessionMergePending; 3318 } 3319 3320 /** 3321 * Sets flag indicating whether the call session is pending merge into a conference or not. 3322 * 3323 * @param callSessionMergePending {@code true} if a merge into the conference is pending, 3324 * {@code false} otherwise. 3325 */ 3326 private void setCallSessionMergePending(boolean callSessionMergePending) { 3327 mCallSessionMergePending = callSessionMergePending; 3328 } 3329 3330 /** 3331 * Determines if there is a conference merge in process. If there is a merge in process, 3332 * determines if both the merge host and peer sessions have completed the merge process. This 3333 * means that we have received terminate or hold signals for the sessions, indicating that they 3334 * are no longer in the process of being merged into the conference. 3335 * <p> 3336 * The sessions are considered to have merged if: both calls still have merge peer/host 3337 * relationships configured, both sessions are not waiting to be merged into the conference, 3338 * and the transient conference session is alive in the case of an initial conference. 3339 * 3340 * @return {@code true} where the host and peer sessions have finished merging into the 3341 * conference, {@code false} if the merge has not yet completed, and {@code false} if there 3342 * is no conference merge in progress. 3343 */ 3344 private boolean shouldProcessConferenceResult() { 3345 boolean areMergeTriggersDone = false; 3346 3347 synchronized (ImsCall.this) { 3348 // if there is a merge going on, then the merge host/peer relationships should have been 3349 // set up. This works for both the initial conference or merging a call into an 3350 // existing conference. 3351 if (!isMergeHost() && !isMergePeer()) { 3352 if (CONF_DBG) { 3353 loge("shouldProcessConferenceResult :: no merge in progress"); 3354 } 3355 return false; 3356 } 3357 3358 // There is a merge in progress, so check the sessions to ensure: 3359 // 1. Both calls have completed being merged (or failing to merge) into the conference. 3360 // 2. The transient conference session is alive. 3361 if (isMergeHost()) { 3362 if (CONF_DBG) { 3363 logi("shouldProcessConferenceResult :: We are a merge host"); 3364 logi("shouldProcessConferenceResult :: Here is the merge peer=" + mMergePeer); 3365 } 3366 areMergeTriggersDone = !isCallSessionMergePending() && 3367 !mMergePeer.isCallSessionMergePending(); 3368 if (!isMultiparty()) { 3369 // Only check the transient session when there is no existing conference 3370 areMergeTriggersDone &= isSessionAlive(mTransientConferenceSession); 3371 } 3372 } else if (isMergePeer()) { 3373 if (CONF_DBG) { 3374 logi("shouldProcessConferenceResult :: We are a merge peer"); 3375 logi("shouldProcessConferenceResult :: Here is the merge host=" + mMergeHost); 3376 } 3377 areMergeTriggersDone = !isCallSessionMergePending() && 3378 !mMergeHost.isCallSessionMergePending(); 3379 if (!mMergeHost.isMultiparty()) { 3380 // Only check the transient session when there is no existing conference 3381 areMergeTriggersDone &= isSessionAlive(mMergeHost.mTransientConferenceSession); 3382 } else { 3383 // This else block is a special case for Verizon to handle these steps 3384 // 1. Establish a conference call. 3385 // 2. Add a new call (conference in in BG) 3386 // 3. Swap (conference active on FG) 3387 // 4. Merge 3388 // What happens here is that the BG call gets a terminated callback 3389 // because it was added to the conference. I've seen where 3390 // the FG gets no callback at all because its already active. 3391 // So if we continue to wait for it to set its isCallSessionMerging 3392 // flag to false...we'll be waiting forever. 3393 areMergeTriggersDone = !isCallSessionMergePending(); 3394 } 3395 } else { 3396 // Realistically this shouldn't happen, but best to be safe. 3397 loge("shouldProcessConferenceResult : merge in progress but call is neither" + 3398 " host nor peer."); 3399 } 3400 if (CONF_DBG) { 3401 logi("shouldProcessConferenceResult :: returning:" + 3402 (areMergeTriggersDone ? "true" : "false")); 3403 } 3404 } 3405 return areMergeTriggersDone; 3406 } 3407 3408 /** 3409 * Provides a string representation of the {@link ImsCall}. Primarily intended for use in log 3410 * statements. 3411 * 3412 * @return String representation of call. 3413 */ 3414 @Override 3415 public String toString() { 3416 StringBuilder sb = new StringBuilder(); 3417 sb.append("[ImsCall objId:"); 3418 sb.append(System.identityHashCode(this)); 3419 sb.append(" onHold:"); 3420 sb.append(isOnHold() ? "Y" : "N"); 3421 sb.append(" mute:"); 3422 sb.append(isMuted() ? "Y" : "N"); 3423 if (mCallProfile != null) { 3424 sb.append(" mCallProfile:" + mCallProfile); 3425 sb.append(" tech:"); 3426 sb.append(mCallProfile.getCallExtra(ImsCallProfile.EXTRA_CALL_RAT_TYPE)); 3427 } 3428 sb.append(" updateRequest:"); 3429 sb.append(updateRequestToString(mUpdateRequest)); 3430 sb.append(" merging:"); 3431 sb.append(isMerging() ? "Y" : "N"); 3432 if (isMerging()) { 3433 if (isMergePeer()) { 3434 sb.append("P"); 3435 } else { 3436 sb.append("H"); 3437 } 3438 } 3439 sb.append(" merge action pending:"); 3440 sb.append(isCallSessionMergePending() ? "Y" : "N"); 3441 sb.append(" merged:"); 3442 sb.append(isMerged() ? "Y" : "N"); 3443 sb.append(" multiParty:"); 3444 sb.append(isMultiparty() ? "Y" : "N"); 3445 sb.append(" confHost:"); 3446 sb.append(isConferenceHost() ? "Y" : "N"); 3447 sb.append(" buried term:"); 3448 sb.append(mSessionEndDuringMerge ? "Y" : "N"); 3449 sb.append(" isVideo: "); 3450 sb.append(isVideoCall() ? "Y" : "N"); 3451 sb.append(" wasVideo: "); 3452 sb.append(mWasVideoCall ? "Y" : "N"); 3453 sb.append(" isWifi: "); 3454 sb.append(isWifiCall() ? "Y" : "N"); 3455 sb.append(" session:"); 3456 sb.append(mSession); 3457 sb.append(" transientSession:"); 3458 sb.append(mTransientConferenceSession); 3459 sb.append("]"); 3460 return sb.toString(); 3461 } 3462 3463 private void throwImsException(Throwable t, int code) throws ImsException { 3464 if (t instanceof ImsException) { 3465 throw (ImsException) t; 3466 } else { 3467 throw new ImsException(String.valueOf(code), t, code); 3468 } 3469 } 3470 3471 /** 3472 * Append the ImsCall information to the provided string. Usefull for as a logging helper. 3473 * @param s The original string 3474 * @return The original string with {@code ImsCall} information appended to it. 3475 */ 3476 private String appendImsCallInfoToString(String s) { 3477 StringBuilder sb = new StringBuilder(); 3478 sb.append(s); 3479 sb.append(" ImsCall="); 3480 sb.append(ImsCall.this); 3481 return sb.toString(); 3482 } 3483 3484 /** 3485 * Updates {@link #mWasVideoCall} based on the current {@link ImsCallProfile} for the call. 3486 * 3487 * @param profile The current {@link ImsCallProfile} for the call. 3488 */ 3489 private void trackVideoStateHistory(ImsCallProfile profile) { 3490 mWasVideoCall = mWasVideoCall || profile.isVideoCall(); 3491 } 3492 3493 /** 3494 * @return {@code true} if this call was a video call at some point in its life span, 3495 * {@code false} otherwise. 3496 */ 3497 public boolean wasVideoCall() { 3498 return mWasVideoCall; 3499 } 3500 3501 /** 3502 * @return {@code true} if this call is a video call, {@code false} otherwise. 3503 */ 3504 public boolean isVideoCall() { 3505 synchronized(mLockObj) { 3506 return mCallProfile != null && mCallProfile.isVideoCall(); 3507 } 3508 } 3509 3510 /** 3511 * Determines if the current call radio access technology is over WIFI. 3512 * Note: This depends on the RIL exposing the {@link ImsCallProfile#EXTRA_CALL_RAT_TYPE} extra. 3513 * This method is primarily intended to be used when checking if answering an incoming audio 3514 * call should cause a wifi video call to drop (e.g. 3515 * {@link android.telephony.CarrierConfigManager# 3516 * KEY_DROP_VIDEO_CALL_WHEN_ANSWERING_AUDIO_CALL_BOOL} is set). 3517 * 3518 * @return {@code true} if the call is over WIFI, {@code false} otherwise. 3519 */ 3520 public boolean isWifiCall() { 3521 synchronized(mLockObj) { 3522 if (mCallProfile == null) { 3523 return false; 3524 } 3525 int radioTechnology = getRadioTechnology(); 3526 return radioTechnology == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN; 3527 } 3528 } 3529 3530 /** 3531 * Determines the radio access technology for the {@link ImsCall}. 3532 * @return The {@link ServiceState} {@code RIL_RADIO_TECHNOLOGY_*} code in use. 3533 */ 3534 public int getRadioTechnology() { 3535 synchronized(mLockObj) { 3536 if (mCallProfile == null) { 3537 return ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN; 3538 } 3539 String callType = mCallProfile.getCallExtra(ImsCallProfile.EXTRA_CALL_RAT_TYPE); 3540 if (callType == null || callType.isEmpty()) { 3541 callType = mCallProfile.getCallExtra(ImsCallProfile.EXTRA_CALL_RAT_TYPE_ALT); 3542 } 3543 3544 // The RIL (sadly) sends us the EXTRA_CALL_RAT_TYPE as a string extra, rather than an 3545 // integer extra, so we need to parse it. 3546 int radioTechnology = ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN; 3547 try { 3548 radioTechnology = Integer.parseInt(callType); 3549 } catch (NumberFormatException nfe) { 3550 radioTechnology = ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN; 3551 } 3552 3553 return radioTechnology; 3554 } 3555 } 3556 3557 /** 3558 * Log a string to the radio buffer at the info level. 3559 * @param s The message to log 3560 */ 3561 private void logi(String s) { 3562 Log.i(TAG, appendImsCallInfoToString(s)); 3563 } 3564 3565 /** 3566 * Log a string to the radio buffer at the debug level. 3567 * @param s The message to log 3568 */ 3569 private void logd(String s) { 3570 Log.d(TAG, appendImsCallInfoToString(s)); 3571 } 3572 3573 /** 3574 * Log a string to the radio buffer at the verbose level. 3575 * @param s The message to log 3576 */ 3577 private void logv(String s) { 3578 Log.v(TAG, appendImsCallInfoToString(s)); 3579 } 3580 3581 /** 3582 * Log a string to the radio buffer at the error level. 3583 * @param s The message to log 3584 */ 3585 private void loge(String s) { 3586 Log.e(TAG, appendImsCallInfoToString(s)); 3587 } 3588 3589 /** 3590 * Log a string to the radio buffer at the error level with a throwable 3591 * @param s The message to log 3592 * @param t The associated throwable 3593 */ 3594 private void loge(String s, Throwable t) { 3595 Log.e(TAG, appendImsCallInfoToString(s), t); 3596 } 3597 } 3598