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