FileDocCategorySizeDatePackage
ImsCall.javaAPI DocAndroid 5.1 API108515Thu Mar 12 22:22:52 GMT 2015com.android.ims

ImsCall

public class ImsCall extends Object implements com.android.ims.internal.ICall
Handles an IMS voice / video call over LTE. You can instantiate this class with {@link ImsManager}.
hide

Fields Summary
public static final int
USSD_MODE_NOTIFY
public static final int
USSD_MODE_REQUEST
private static final String
TAG
private static final boolean
FORCE_DEBUG
private static final boolean
DBG
private static final boolean
VDBG
private static final int
UPDATE_NONE
private static final int
UPDATE_HOLD
private static final int
UPDATE_HOLD_MERGE
private static final int
UPDATE_RESUME
private static final int
UPDATE_MERGE
private static final int
UPDATE_EXTEND_TO_CONFERENCE
private static final int
UPDATE_UNSPECIFIED
private Object
mLockObj
private android.content.Context
mContext
private boolean
mInCall
private boolean
mHold
private boolean
mMute
private int
mUpdateRequest
private Listener
mListener
private ImsCall
mMergePeer
private ImsCall
mMergeHost
private com.android.ims.internal.ImsCallSession
mSession
private ImsCallProfile
mCallProfile
private ImsCallProfile
mProposedCallProfile
private ImsReasonInfo
mLastReasonInfo
private com.android.ims.internal.ImsStreamMediaSession
mMediaSession
private com.android.ims.internal.ImsCallSession
mTransientConferenceSession
private boolean
mSessionEndDuringMerge
private ImsReasonInfo
mSessionEndDuringMergeReasonInfo
private boolean
mIsMerged
private boolean
mCallSessionMergePending
Constructors Summary
public ImsCall(android.content.Context context, ImsCallProfile profile)
Create an IMS call object.

param
context the context for accessing system services
param
profile the call profile to make/take a call


                               
         
        mContext = context;
        mCallProfile = profile;
    
Methods Summary
public voidaccept(int callType)
Accepts a call.

see
Listener#onCallStarted
param
callType The call type the user agreed to for accepting the call.
throws
ImsException if the IMS service fails to accept the call

        if (VDBG) {
            log("accept ::");
        }

        accept(callType, new ImsStreamMediaProfile());
    
public voidaccept(int callType, ImsStreamMediaProfile profile)
Accepts a call.

param
callType call type to be answered in {@link ImsCallProfile}
param
profile a media profile to be answered (audio/audio & video, direction, ...)
see
Listener#onCallStarted
throws
ImsException if the IMS service fails to accept the call

        if (VDBG) {
            log("accept :: callType=" + callType + ", profile=" + profile);
        }

        synchronized(mLockObj) {
            if (mSession == null) {
                throw new ImsException("No call to answer",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            try {
                mSession.accept(callType, profile);
            } catch (Throwable t) {
                loge("accept :: ", t);
                throw new ImsException("accept()", t, 0);
            }

            if (mInCall && (mProposedCallProfile != null)) {
                if (DBG) {
                    log("accept :: call profile will be updated");
                }

                mCallProfile = mProposedCallProfile;
                mProposedCallProfile = null;
            }

            // Other call update received
            if (mInCall && (mUpdateRequest == UPDATE_UNSPECIFIED)) {
                mUpdateRequest = UPDATE_NONE;
            }
        }
    
public voidattachSession(com.android.ims.internal.ImsCallSession session)
Attaches an incoming call to this call object.

param
session the session that receives the incoming call
throws
ImsException if the IMS service fails to attach this object to the session

         if (DBG) {
             log("attachSession :: session=" + session);
         }

         synchronized(mLockObj) {
             mSession = session;

             try {
                 mSession.setListener(createCallSessionListener());
             } catch (Throwable t) {
                 loge("attachSession :: ", t);
                 throwImsException(t, 0);
             }
         }
     
public booleancheckIfRemoteUserIsSame(java.lang.String userId)
Checks if the call has a same remote user identity or not.

param
userId the remote user identity
return
true if the remote user identity is equal; otherwise, false

        if (userId == null) {
            return false;
        }

        return userId.equals(mCallProfile.getCallExtra(ImsCallProfile.EXTRA_REMOTE_URI, ""));
    
private voidclear(ImsReasonInfo lastReasonInfo)

        mInCall = false;
        mHold = false;
        mUpdateRequest = UPDATE_NONE;
        mLastReasonInfo = lastReasonInfo;
    
private voidclearMergeInfo()
Clears the merge peer for this call, ensuring that the peer's connection to this call is also severed at the same time.

        if (VDBG) {
            log("clearMergeInfo :: clearing all merge info");
        }

        // First clear out the merge partner then clear ourselves out.
        if (mMergeHost != null) {
            mMergeHost.mMergePeer = null;
            mMergeHost.mUpdateRequest = UPDATE_NONE;
            mMergeHost.mCallSessionMergePending = false;
        }
        if (mMergePeer != null) {
            mMergePeer.mMergeHost = null;
            mMergePeer.mUpdateRequest = UPDATE_NONE;
            mMergePeer.mCallSessionMergePending = false;
        }
        mMergeHost = null;
        mMergePeer = null;
        mUpdateRequest = UPDATE_NONE;
        mCallSessionMergePending = false;
    
private voidclearSessionTerminationFlags()

        mSessionEndDuringMerge = false;
        mSessionEndDuringMergeReasonInfo = null;
    
public voidclose()
Closes this object. This object is not usable after being closed.

        synchronized(mLockObj) {
            if (mSession != null) {
                mSession.close();
                mSession = null;
            }

            mCallProfile = null;
            mProposedCallProfile = null;
            mLastReasonInfo = null;
            mMediaSession = null;
        }
    
public voidconferenceStateUpdated(ImsConferenceState state)
Report a new conference state to the current {@link ImsCall} and inform listeners of the change. Marked as {@code VisibleForTesting} so that the {@code com.android.internal.telephony.TelephonyTester} class can inject a test conference event package into a regular ongoing IMS call.

param
state The {@link ImsConferenceState}.

        Listener listener;

        synchronized(this) {
            notifyConferenceStateUpdated(state);
            listener = mListener;
        }

        if (listener != null) {
            try {
                listener.onCallConferenceStateUpdated(this, state);
            } catch (Throwable t) {
                loge("callSessionConferenceStateUpdated :: ", t);
            }
        }
    
private ImsCallSession.ListenercreateCallSessionListener()
Creates an IMS call session listener.

        return new ImsCallSessionListenerProxy();
    
private ImsStreamMediaProfilecreateHoldMediaProfile()

        ImsStreamMediaProfile mediaProfile = new ImsStreamMediaProfile();

        if (mCallProfile == null) {
            return mediaProfile;
        }

        mediaProfile.mAudioQuality = mCallProfile.mMediaProfile.mAudioQuality;
        mediaProfile.mVideoQuality = mCallProfile.mMediaProfile.mVideoQuality;
        mediaProfile.mAudioDirection = ImsStreamMediaProfile.DIRECTION_SEND;

        if (mediaProfile.mVideoQuality != ImsStreamMediaProfile.VIDEO_QUALITY_NONE) {
            mediaProfile.mVideoDirection = ImsStreamMediaProfile.DIRECTION_SEND;
        }

        return mediaProfile;
    
private com.android.ims.ImsCallcreateNewCall(com.android.ims.internal.ImsCallSession session, ImsCallProfile profile)

        ImsCall call = new ImsCall(mContext, profile);

        try {
            call.attachSession(session);
        } catch (ImsException e) {
            if (call != null) {
                call.close();
                call = null;
            }
        }

        // Do additional operations...

        return call;
    
private ImsStreamMediaProfilecreateResumeMediaProfile()

        ImsStreamMediaProfile mediaProfile = new ImsStreamMediaProfile();

        if (mCallProfile == null) {
            return mediaProfile;
        }

        mediaProfile.mAudioQuality = mCallProfile.mMediaProfile.mAudioQuality;
        mediaProfile.mVideoQuality = mCallProfile.mMediaProfile.mVideoQuality;
        mediaProfile.mAudioDirection = ImsStreamMediaProfile.DIRECTION_SEND_RECEIVE;

        if (mediaProfile.mVideoQuality != ImsStreamMediaProfile.VIDEO_QUALITY_NONE) {
            mediaProfile.mVideoDirection = ImsStreamMediaProfile.DIRECTION_SEND_RECEIVE;
        }

        return mediaProfile;
    
private voidenforceConversationMode()

        if (mInCall) {
            mHold = false;
            mUpdateRequest = UPDATE_NONE;
        }
    
public booleanequalsTo(com.android.ims.internal.ICall call)
Checks if the call is equal or not.

param
call the call to be compared
return
true if the call is equal; otherwise, false

        if (call == null) {
            return false;
        }

        if (call instanceof ImsCall) {
            return this.equals(call);
        }

        return false;
    
public voidextendToConference(java.lang.String[] participants)
Extends this call (1-to-1 call) to the conference call inviting the specified participants to.

        if (VDBG) {
            log("extendToConference ::");
        }

        if (isOnHold()) {
            if (DBG) {
                log("extendToConference :: call is on hold");
            }
            throw new ImsException("Not in a call to extend a call to conference",
                    ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE);
        }

        synchronized(mLockObj) {
            if (mUpdateRequest != UPDATE_NONE) {
                if (DBG) {
                    log("extendToConference :: update is in progress; request=" +
                            updateRequestToString(mUpdateRequest));
                }
                throw new ImsException("Call update is in progress",
                        ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE);
            }

            if (mSession == null) {
                loge("extendToConference :: ");
                throw new ImsException("No call session",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            mSession.extendToConference(participants);
            mUpdateRequest = UPDATE_EXTEND_TO_CONFERENCE;
        }
    
public java.lang.StringgetCallExtra(java.lang.String name)
Gets the specified property of this call.

param
name key to get the extra call information defined in {@link ImsCallProfile}
return
the extra call information as string

        // Lookup the cache

        synchronized(mLockObj) {
            // If not found, try to get the property from the remote
            if (mSession == null) {
                throw new ImsException("No call session",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            try {
                return mSession.getProperty(name);
            } catch (Throwable t) {
                loge("getCallExtra :: ", t);
                throw new ImsException("getCallExtra()", t, 0);
            }
        }
    
public ImsCallProfilegetCallProfile()
Gets the negotiated (local & remote) call profile.

return
a {@link ImsCallProfile} object that has the negotiated call profile

        synchronized(mLockObj) {
            return mCallProfile;
        }
    
public com.android.ims.internal.ImsCallSessiongetCallSession()
Gets the {@link ImsCallSession} that carries this call.

return
the session object that carries this call
hide

        synchronized(mLockObj) {
            return mSession;
        }
    
public ImsReasonInfogetLastReasonInfo()
Gets the last reason information when the call is not established, cancelled or terminated.

return
the last reason information

        synchronized(mLockObj) {
            return mLastReasonInfo;
        }
    
public ImsCallProfilegetLocalCallProfile()
Gets the local call profile (local capabilities).

return
a {@link ImsCallProfile} object that has the local call profile

        synchronized(mLockObj) {
            if (mSession == null) {
                throw new ImsException("No call session",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            try {
                return mSession.getLocalCallProfile();
            } catch (Throwable t) {
                loge("getLocalCallProfile :: ", t);
                throw new ImsException("getLocalCallProfile()", t, 0);
            }
        }
    
public com.android.ims.internal.ImsStreamMediaSessiongetMediaSession()
Gets the {@link ImsStreamMediaSession} that handles the media operation of this call. Almost interface APIs are for the VT (Video Telephony).

return
the media session object that handles the media operation of this call
hide

        synchronized(mLockObj) {
            return mMediaSession;
        }
    
public ImsCallProfilegetProposedCallProfile()
Gets the call profile proposed by the local/remote user.

return
a {@link ImsCallProfile} object that has the proposed call profile

        synchronized(mLockObj) {
            if (!isInCall()) {
                return null;
            }

            return mProposedCallProfile;
        }
    
public ImsCallProfilegetRemoteCallProfile()
Gets the remote call profile (remote capabilities).

return
a {@link ImsCallProfile} object that has the remote call profile

        synchronized(mLockObj) {
            if (mSession == null) {
                throw new ImsException("No call session",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            try {
                return mSession.getRemoteCallProfile();
            } catch (Throwable t) {
                loge("getRemoteCallProfile :: ", t);
                throw new ImsException("getRemoteCallProfile()", t, 0);
            }
        }
    
public intgetState()
Gets the state of the {@link ImsCallSession} that carries this call. The value returned must be one of the states in {@link ImsCallSession#State}.

return
the session state

        synchronized(mLockObj) {
            if (mSession == null) {
                return ImsCallSession.State.IDLE;
            }

            return mSession.getState();
        }
    
public booleanhasPendingUpdate()
Checks if the call has a pending update operation.

return
true if the call has a pending update operation

        synchronized(mLockObj) {
            return (mUpdateRequest != UPDATE_NONE);
        }
    
public voidhold()
Puts a call on hold. When succeeds, {@link Listener#onCallHeld} is called.

see
Listener#onCallHeld, Listener#onCallHoldFailed
throws
ImsException if the IMS service fails to hold the call

        if (VDBG) {
            log("hold :: ImsCall=" + this);
        }

        if (isOnHold()) {
            if (DBG) {
                log("hold :: call is already on hold");
            }
            return;
        }

        synchronized(mLockObj) {
            if (mUpdateRequest != UPDATE_NONE) {
                loge("hold :: update is in progress; request=" +
                        updateRequestToString(mUpdateRequest));
                throw new ImsException("Call update is in progress",
                        ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE);
            }

            if (mSession == null) {
                loge("hold :: ");
                throw new ImsException("No call session",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            mSession.hold(createHoldMediaProfile());
            // FIXME: update the state on the callback?
            mHold = true;
            mUpdateRequest = UPDATE_HOLD;
        }
    
public voidinviteParticipants(java.lang.String[] participants)
Requests the conference server to invite an additional participants to the conference.

        if (VDBG) {
            log("inviteParticipants ::");
        }

        synchronized(mLockObj) {
            if (mSession == null) {
                loge("inviteParticipants :: ");
                throw new ImsException("No call session",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            mSession.inviteParticipants(participants);
        }
    
private booleanisCallSessionMergePending()
Determines if the call session is pending merge into a conference or not.

return
{@code true} if a merge into a conference is pending, {@code false} otherwise.

        return mCallSessionMergePending;
    
public booleanisInCall()
Checks if the call is established.

return
true if the call is established

        synchronized(mLockObj) {
            return mInCall;
        }
    
private booleanisMergeHost()
Determines if the current call is the host of the merge.

return
{@code true} if the call is the merge host.

        return mMergePeer != null && mMergeHost == null;
    
private booleanisMergePeer()
Determines if the current call is the peer of the merge.

return
{@code true} if the call is the merge peer.

        return mMergePeer == null && mMergeHost != null;
    
public booleanisMerged()

return
{@code true} if the call recently merged into a conference call.

        return mIsMerged;
    
private booleanisMerging()
Determines if the current call is in the process of merging with another call or conference.

return
{@code true} if in the process of merging.

        return mMergePeer != null || mMergeHost != null;
    
public booleanisMultiparty()
Determines if the call is a multiparty call.

return
{@code True} if the call is a multiparty call.

        synchronized(mLockObj) {
            if (mSession == null) {
                return false;
            }

            return mSession.isMultiparty();
        }
    
public booleanisMuted()
Checks if the call is muted.

return
true if the call is muted

        synchronized(mLockObj) {
            return mMute;
        }
    
public booleanisOnHold()
Checks if the call is on hold.

return
true if the call is on hold

        synchronized(mLockObj) {
            return mHold;
        }
    
public static booleanisSessionAlive(com.android.ims.internal.ImsCallSession session)

        return session != null && session.isAlive();
    
private booleanisTransientConferenceSession(com.android.ims.internal.ImsCallSession session)
This function determines if the ImsCallSession is our actual ImsCallSession or if is the transient session used in the process of creating a conference. This function should only be called within callbacks that are not directly related to conference merging but might potentially still be called on the transient ImsCallSession sent to us from callSessionMergeStarted() when we don't really care. In those situations, we probably don't want to take any action so we need to know that we can return early.

param
session - The {@link ImsCallSession} that the function needs to analyze
return
true if this is the transient {@link ImsCallSession}, false otherwise.

        if (session != null && session != mSession && session == mTransientConferenceSession) {
            return true;
        }
        return false;
    
private voidlog(java.lang.String s)

        Rlog.d(TAG, s);
    
private voidloge(java.lang.String s)

        Rlog.e(TAG, s);
    
private voidloge(java.lang.String s, java.lang.Throwable t)

        Rlog.e(TAG, s, t);
    
private voidlogv(java.lang.String s)
Logs the specified message, as well as the current instance of {@link ImsCall}.

param
s The message to log.

        StringBuilder sb = new StringBuilder();
        sb.append(s);
        sb.append(" imsCall=");
        sb.append(ImsCall.this);
        Rlog.v(TAG, sb.toString());
    
private voidmerge()
Merges the active & hold call.

see
Listener#onCallMerged, Listener#onCallMergeFailed
throws
ImsException if the IMS service fails to merge the call

        if (VDBG) {
            log("merge :: ImsCall=" + this);
        }

        synchronized(mLockObj) {
            if (mUpdateRequest != UPDATE_NONE) {
                loge("merge :: update is in progress; request=" +
                        updateRequestToString(mUpdateRequest));
                throw new ImsException("Call update is in progress",
                        ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE);
            }

            if (mSession == null) {
                loge("merge :: no call session");
                throw new ImsException("No call session",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            // if skipHoldBeforeMerge = true, IMS service implementation will
            // merge without explicitly holding the call.
            if (mHold || (mContext.getResources().getBoolean(
                    com.android.internal.R.bool.skipHoldBeforeMerge))) {

                if (mMergePeer != null && !mMergePeer.isMultiparty() && !isMultiparty()) {
                    // We only set UPDATE_MERGE when we are adding the first
                    // calls to the Conference.  If there is already a conference
                    // no special handling is needed. The existing conference
                    // session will just go active and any other sessions will be terminated
                    // if needed.  There will be no merge failed callback.
                    // Mark both the host and peer UPDATE_MERGE to ensure both are aware that a
                    // merge is pending.
                    mUpdateRequest = UPDATE_MERGE;
                    mMergePeer.mUpdateRequest = UPDATE_MERGE;
                }

                mSession.merge();
            } else {
                // This code basically says, we need to explicitly hold before requesting a merge
                // when we get the callback that the hold was successful (or failed), we should
                // automatically request a merge.
                mSession.hold(createHoldMediaProfile());
                mHold = true;
                mUpdateRequest = UPDATE_HOLD_MERGE;
            }
        }
    
public voidmerge(com.android.ims.ImsCall bgCall)
Merges the active & hold call.

param
bgCall the background (holding) call
see
Listener#onCallMerged, Listener#onCallMergeFailed
throws
ImsException if the IMS service fails to merge the call

        if (VDBG) {
            log("merge(1) :: bgImsCall=" + bgCall);
        }

        if (bgCall == null) {
            throw new ImsException("No background call",
                    ImsReasonInfo.CODE_LOCAL_ILLEGAL_ARGUMENT);
        }

        synchronized(mLockObj) {
            // Mark both sessions as pending merge.
            this.setCallSessionMergePending(true);
            bgCall.setCallSessionMergePending(true);

            if ((!isMultiparty() && !bgCall.isMultiparty()) || isMultiparty()) {
                // If neither call is multiparty, the current call is the merge host and the bg call
                // is the merge peer (ie we're starting a new conference).
                // OR
                // If this call is multiparty, it is the merge host and the other call is the merge
                // peer.
                setMergePeer(bgCall);
            } else {
                // If the bg call is multiparty, it is the merge host.
                setMergeHost(bgCall);
            }
        }
        merge();
    
private voidmergeInternal()

        if (VDBG) {
            log("mergeInternal :: ImsCall=" + this);
        }

        mSession.merge();
        mUpdateRequest = UPDATE_MERGE;
    
private voidnotifyConferenceSessionTerminated(ImsReasonInfo reasonInfo)

        ImsCall.Listener listener = mListener;
        clear(reasonInfo);

        if (listener != null) {
            try {
                listener.onCallTerminated(this, reasonInfo);
            } catch (Throwable t) {
                loge("notifyConferenceSessionTerminated :: ", t);
            }
        }
    
private voidnotifyConferenceStateUpdated(ImsConferenceState state)

        Set<Entry<String, Bundle>> participants = state.mParticipants.entrySet();

        if (participants == null) {
            return;
        }

        Iterator<Entry<String, Bundle>> iterator = participants.iterator();
        List<ConferenceParticipant> conferenceParticipants = new ArrayList<>(participants.size());
        while (iterator.hasNext()) {
            Entry<String, Bundle> entry = iterator.next();

            String key = entry.getKey();
            Bundle confInfo = entry.getValue();
            String status = confInfo.getString(ImsConferenceState.STATUS);
            String user = confInfo.getString(ImsConferenceState.USER);
            String displayName = confInfo.getString(ImsConferenceState.DISPLAY_TEXT);
            String endpoint = confInfo.getString(ImsConferenceState.ENDPOINT);

            if (DBG) {
                log("notifyConferenceStateUpdated :: key=" + key +
                        ", status=" + status +
                        ", user=" + user +
                        ", displayName= " + displayName +
                        ", endpoint=" + endpoint);
            }

            Uri handle = Uri.parse(user);
            Uri endpointUri = Uri.parse(endpoint);
            int connectionState = ImsConferenceState.getConnectionStateForStatus(status);

            ConferenceParticipant conferenceParticipant = new ConferenceParticipant(handle,
                    displayName, endpointUri, connectionState);
            conferenceParticipants.add(conferenceParticipant);
        }

        if (!conferenceParticipants.isEmpty() && mListener != null) {
            try {
                mListener.onConferenceParticipantsStateChanged(this, conferenceParticipants);
            } catch (Throwable t) {
                loge("notifyConferenceStateUpdated :: ", t);
            }
        }
    
private voidnotifyError(int reason, int statusCode, java.lang.String message)

    
private voidnotifySessionTerminatedDuringMerge()
Handles the case where the session has ended during a merge by reporting the termination reason to listeners.

        ImsCall.Listener listener;
        boolean notifyFailure = false;
        ImsReasonInfo notifyFailureReasonInfo = null;

        synchronized(ImsCall.this) {
            listener = mListener;
            if (mSessionEndDuringMerge) {
                // Set some local variables that will send out a notification about a
                // previously buried termination callback for our primary session now that
                // we know that this is not due to the conference call merging successfully.
                if (DBG) {
                    log("notifySessionTerminatedDuringMerge ::reporting terminate during merge");
                }
                notifyFailure = true;
                notifyFailureReasonInfo = mSessionEndDuringMergeReasonInfo;
            }
            clearSessionTerminationFlags();
        }

        if (listener != null && notifyFailure) {
            try {
                processCallTerminated(notifyFailureReasonInfo);
            } catch (Throwable t) {
                loge("notifySessionTerminatedDuringMerge :: ", t);
            }
        }
    
private voidprocessCallTerminated(ImsReasonInfo reasonInfo)
Perform all cleanup and notification around the termination of a session. Note that there are 2 distinct modes of operation. The first is when we receive a session termination on the primary session when we are in the processing of merging. The second is when we are not merging anything and the call is terminated.

param
reasonInfo The reason for the session termination

        if (VDBG) {
            String reasonString = reasonInfo != null ? reasonInfo.toString() : "null";
            log("processCallTerminated :: ImsCall=" + this + " reason=" + reasonString);
        }

        ImsCall.Listener listener = null;

        synchronized(ImsCall.this) {
            // If we are in the midst of establishing a conference, we will bury the termination
            // until the merge has completed.  If necessary we can surface the termination at this
            // point.
            if (isCallSessionMergePending()) {
                // Since we are in the process of a merge, this trigger means something
                // else because it is probably due to the merge happening vs. the
                // session is really terminated. Let's flag this and revisit if
                // the merge() ends up failing because we will need to take action on the
                // mSession in that case since the termination was not due to the merge
                // succeeding.
                if (DBG) {
                    log("processCallTerminated :: burying termination during ongoing merge.");
                }
                mSessionEndDuringMerge = true;
                mSessionEndDuringMergeReasonInfo = reasonInfo;
                return;
            }

            // If we are terminating the conference call, notify using conference listeners.
            if (isMultiparty()) {
                notifyConferenceSessionTerminated(reasonInfo);
                return;
            } else {
                listener = mListener;
                clear(reasonInfo);
            }
        }

        if (listener != null) {
            try {
                listener.onCallTerminated(ImsCall.this, reasonInfo);
            } catch (Throwable t) {
                loge("processCallTerminated :: ", t);
            }
        }
    
private voidprocessMergeComplete()
We have detected that a initial conference call has been fully configured. The internal state of both {@code ImsCall} objects need to be cleaned up to reflect the new state. This function should only be called in the context of the merge host to simplify logic

        if (VDBG) {
            log("processMergeComplete :: ImsCall=" + this);
        }

        // The logic simplifies if we can assume that this function is only called on
        // the merge host.
        if (!isMergeHost()) {
            loge("processMergeComplete :: We are not the merge host!");
            return;
        }

        ImsCall.Listener listener;
        boolean swapRequired = false;
        synchronized(ImsCall.this) {
            ImsCall finalHostCall = this;
            ImsCall finalPeerCall = mMergePeer;

            if (isMultiparty()) {
                // The only clean up that we need to do for a merge into an existing conference
                // is to deal with the disconnect of the peer if it was successfully added to
                // the conference.
                setIsMerged(false);
                if (!isSessionAlive(mMergePeer.mSession)) {
                    // If the peer is dead, let's not play a disconnect sound for it when we
                    // unbury the termination callback.
                    mMergePeer.setIsMerged(true);
                } else {
                    mMergePeer.setIsMerged(false);
                }
            } else {
                // If we are here, we are not trying to merge a new call into an existing
                // conference.  That means that there is a transient session on the merge
                // host that represents the future conference once all the parties
                // have been added to it.  So make sure that it exists or else something
                // very wrong is going on.
                if (mTransientConferenceSession == null) {
                    loge("processMergeComplete :: No transient session!");
                    return;
                }
                if (mMergePeer == null) {
                    loge("processMergeComplete :: No merge peer!");
                    return;
                }

                // Since we are the host, we have the transient session attached to us. Let's detach
                // it and figure out where we need to set it for the final conference configuration.
                ImsCallSession transientConferenceSession = mTransientConferenceSession;
                mTransientConferenceSession = null;

                // Clear the listener for this transient session, we'll create a new listener
                // when it is attached to the final ImsCall that it should live on.
                transientConferenceSession.setListener(null);

                // Determine which call the transient session should be moved to.  If the current
                // call session is still alive and the merge peer's session is not, we have a
                // situation where the current call failed to merge into the conference but the
                // merge peer did merge in to the conference.  In this type of scenario the current
                // call will continue as a single party call, yet the background call will become
                // the conference.

                if (isSessionAlive(mSession) && !isSessionAlive(mMergePeer.getCallSession())) {
                    // I'm the host but we are moving the transient session to the peer since its
                    // session was disconnected and my session is still alive.  This signifies that
                    // their session was properly added to the conference but mine was not because
                    // it is probably in the held state as opposed to part of the final conference.
                    // In this case, we need to set isMerged to false on both calls so the
                    // disconnect sound is called when either call disconnects.
                    // Note that this case is only valid if this is an initial conference being
                    // brought up.
                    finalHostCall = mMergePeer;
                    finalPeerCall = this;
                    swapRequired = true;
                    setIsMerged(false);
                    mMergePeer.setIsMerged(false);
                    if (VDBG) {
                        log("processMergeComplete :: transient will transfer to merge peer");
                    }
                } else if (!isSessionAlive(mSession) && isSessionAlive(mMergePeer.getCallSession())) {
                    // The transient session stays with us and the disconnect sound should be played
                    // when the merge peer eventually disconnects since it was not actually added to
                    // the conference and is probably sitting in the held state.
                    finalHostCall = this;
                    finalPeerCall = mMergePeer;
                    swapRequired = false;
                    setIsMerged(false);
                    mMergePeer.setIsMerged(false); // Play the disconnect sound
                    if (VDBG) {
                        log("processMergeComplete :: transient will stay with the merge host");
                    }
                } else {
                    // The transient session stays with us and the disconnect sound should not be
                    // played when we ripple up the disconnect for the merge peer because it was
                    // only disconnected to be added to the conference.
                    finalHostCall = this;
                    finalPeerCall = mMergePeer;
                    swapRequired = false;
                    setIsMerged(false);
                    mMergePeer.setIsMerged(true);
                    if (VDBG) {
                        log("processMergeComplete :: transient will stay with us (I'm the host).");
                    }
                }

                if (VDBG) {
                    log("processMergeComplete :: call=" + finalHostCall + " is the final host");
                }

                // Add the transient session to the ImsCall that ended up being the host for the
                // conference.
                finalHostCall.setTransientSessionAsPrimary(transientConferenceSession);
            }

            listener = finalHostCall.mListener;

            // Clear all the merge related flags.
            clearMergeInfo();

            // For the final peer...let's bubble up any possible disconnects that we had
            // during the merge process
            finalPeerCall.notifySessionTerminatedDuringMerge();
            // For the final host, let's just bury the disconnects that we my have received
            // during the merge process since we are now the host of the conference call.
            finalHostCall.clearSessionTerminationFlags();
        }
        if (listener != null) {
            try {
                listener.onCallMerged(ImsCall.this, swapRequired);
            } catch (Throwable t) {
                loge("processMergeComplete :: ", t);
            }
        }
        return;
    
private voidprocessMergeFailed(ImsReasonInfo reasonInfo)
We received a callback from ImsCallSession that a merge failed. Clean up all internal state to represent this state change. The calling function is a callback and should have been called on the session that was in the foreground when merge() was originally called. It is assumed that this function will be called on the merge host.

param
reasonInfo The {@link ImsReasonInfo} why the merge failed.

        if (VDBG) {
            log("processMergeFailed :: this=" + this + "reason=" + reasonInfo);
        }

        ImsCall.Listener listener;
        synchronized(ImsCall.this) {
            // The logic simplifies if we can assume that this function is only called on
            // the merge host.
            if (!isMergeHost()) {
                loge("processMergeFailed :: We are not the merge host!");
                return;
            }

            if (mMergePeer == null) {
                loge("processMergeFailed :: No merge peer!");
                return;
            }

            if (!isMultiparty()) {
                if (mTransientConferenceSession == null) {
                    loge("processMergeFailed :: No transient session!");
                    return;
                }
                // Clean up any work that we performed on the transient session.
                mTransientConferenceSession.setListener(null);
                mTransientConferenceSession = null;
            }

            // Ensure the calls being conferenced into the conference has isMerged = false.
            setIsMerged(false);
            mMergePeer.setIsMerged(false);

            listener = mListener;

            // Ensure any terminations are surfaced from this session.
            notifySessionTerminatedDuringMerge();
            mMergePeer.notifySessionTerminatedDuringMerge();

            // Clear all the various flags around coordinating this merge.
            clearMergeInfo();
        }
        if (listener != null) {
            try {
                listener.onCallMergeFailed(ImsCall.this, reasonInfo);
            } catch (Throwable t) {
                loge("processMergeFailed :: ", t);
            }
        }

        return;
    
public voidreject(int reason)
Rejects a call.

param
reason reason code to reject an incoming call
see
Listener#onCallStartFailed
throws
ImsException if the IMS service fails to accept the call

        if (VDBG) {
            log("reject :: reason=" + reason);
        }

        synchronized(mLockObj) {
            if (mSession != null) {
                mSession.reject(reason);
            }

            if (mInCall && (mProposedCallProfile != null)) {
                if (DBG) {
                    log("reject :: call profile is not updated; destroy it...");
                }

                mProposedCallProfile = null;
            }

            // Other call update received
            if (mInCall && (mUpdateRequest == UPDATE_UNSPECIFIED)) {
                mUpdateRequest = UPDATE_NONE;
            }
        }
    
public voidremoveParticipants(java.lang.String[] participants)
Requests the conference server to remove the specified participants from the conference.

        if (DBG) {
            log("removeParticipants ::");
        }

        synchronized(mLockObj) {
            if (mSession == null) {
                loge("removeParticipants :: ");
                throw new ImsException("No call session",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            mSession.removeParticipants(participants);
        }
    
public voidresume()
Continues a call that's on hold. When succeeds, {@link Listener#onCallResumed} is called.

see
Listener#onCallResumed, Listener#onCallResumeFailed
throws
ImsException if the IMS service fails to resume the call

        if (VDBG) {
            log("resume :: ImsCall=" + this);
        }

        if (!isOnHold()) {
            if (DBG) {
                log("resume :: call is in conversation");
            }
            return;
        }

        synchronized(mLockObj) {
            if (mUpdateRequest != UPDATE_NONE) {
                loge("resume :: update is in progress; request=" +
                        updateRequestToString(mUpdateRequest));
                throw new ImsException("Call update is in progress",
                        ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE);
            }

            if (mSession == null) {
                loge("resume :: ");
                throw new ImsException("No call session",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            // mHold is set to false in confirmation callback that the
            // ImsCall was resumed.
            mUpdateRequest = UPDATE_RESUME;
            mSession.resume(createResumeMediaProfile());
        }
    
public voidsendDtmf(char c, android.os.Message result)
Sends a DTMF code. According to RFC 2833, event 0 ~ 9 maps to decimal value 0 ~ 9, '*' to 10, '#' to 11, event 'A' ~ 'D' to 12 ~ 15, and event flash to 16. Currently, event flash is not supported.

param
c that represents the DTMF to send. '0' ~ '9', 'A' ~ 'D', '*', '#' are valid inputs.
param
result the result message to send when done.

        if (VDBG) {
            log("sendDtmf :: code=" + c);
        }

        synchronized(mLockObj) {
            if (mSession != null) {
                mSession.sendDtmf(c, result);
            }
        }
    
public voidsendUssd(java.lang.String ussdMessage)
Sends an USSD message.

param
ussdMessage USSD message to send

        if (VDBG) {
            log("sendUssd :: ussdMessage=" + ussdMessage);
        }

        synchronized(mLockObj) {
            if (mSession == null) {
                loge("sendUssd :: ");
                throw new ImsException("No call session",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            mSession.sendUssd(ussdMessage);
        }
    
private voidsetCallSessionMergePending(boolean callSessionMergePending)
Sets flag indicating whether the call session is pending merge into a conference or not.

param
callSessionMergePending {@code true} if a merge into the conference is pending, {@code false} otherwise.

        mCallSessionMergePending = callSessionMergePending;
    
public voidsetIsMerged(boolean isMerged)
Marks whether an IMS call is merged. This should be set {@code true} when the call merges into a conference.

param
isMerged Whether the call is merged.

        mIsMerged = isMerged;
    
public voidsetListener(com.android.ims.ImsCall$Listener listener)
Sets the listener to listen to the IMS call events. The method calls {@link #setListener setListener(listener, false)}.

param
listener to listen to the IMS call events of this object; null to remove listener
see
#setListener(Listener, boolean)

        setListener(listener, false);
    
public voidsetListener(com.android.ims.ImsCall$Listener listener, boolean callbackImmediately)
Sets the listener to listen to the IMS call events. A {@link ImsCall} can only hold one listener at a time. Subsequent calls to this method override the previous listener.

param
listener to listen to the IMS call events of this object; null to remove listener
param
callbackImmediately set to true if the caller wants to be called back immediately on the current state

        boolean inCall;
        boolean onHold;
        int state;
        ImsReasonInfo lastReasonInfo;

        synchronized(mLockObj) {
            mListener = listener;

            if ((listener == null) || !callbackImmediately) {
                return;
            }

            inCall = mInCall;
            onHold = mHold;
            state = getState();
            lastReasonInfo = mLastReasonInfo;
        }

        try {
            if (lastReasonInfo != null) {
                listener.onCallError(this, lastReasonInfo);
            } else if (inCall) {
                if (onHold) {
                    listener.onCallHeld(this);
                } else {
                    listener.onCallStarted(this);
                }
            } else {
                switch (state) {
                    case ImsCallSession.State.ESTABLISHING:
                        listener.onCallProgressing(this);
                        break;
                    case ImsCallSession.State.TERMINATED:
                        listener.onCallTerminated(this, lastReasonInfo);
                        break;
                    default:
                        // Ignore it. There is no action in the other state.
                        break;
                }
            }
        } catch (Throwable t) {
            loge("setListener()", t);
        }
    
public voidsetMergeHost(com.android.ims.ImsCall mergeHost)
Sets the merge hody for the current call. The merge host is the foreground call this call will be merged into. On the merge host, sets the merge peer to be this call.

param
mergeHost The merge host this call will be merged into.

        mMergeHost = mergeHost;
        mMergePeer = null;

        mergeHost.mMergeHost = null;
        mergeHost.mMergePeer = ImsCall.this;
    
private voidsetMergePeer(com.android.ims.ImsCall mergePeer)
Sets the merge peer for the current call. The merge peer is the background call that will be merged into this call. On the merge peer, sets the merge host to be this call.

param
mergePeer The peer call to be merged into this one.

        mMergePeer = mergePeer;
        mMergeHost = null;

        mergePeer.mMergeHost = ImsCall.this;
        mergePeer.mMergePeer = null;
    
public voidsetMute(boolean muted)
Mutes or unmutes the mic for the active call.

param
muted true if the call is muted, false otherwise

        synchronized(mLockObj) {
            if (mMute != muted) {
                mMute = muted;

                try {
                    mSession.setMute(muted);
                } catch (Throwable t) {
                    loge("setMute :: ", t);
                    throwImsException(t, 0);
                }
            }
        }
    
private voidsetTransientSessionAsPrimary(com.android.ims.internal.ImsCallSession transientSession)

        synchronized (ImsCall.this) {
            mSession.setListener(null);
            mSession = transientSession;
            mSession.setListener(createCallSessionListener());
        }
    
private booleanshouldProcessConferenceResult()
Determines if there is a conference merge in process. If there is a merge in process, determines if both the merge host and peer sessions have completed the merge process. This means that we have received terminate or hold signals for the sessions, indicating that they are no longer in the process of being merged into the conference.

The sessions are considered to have merged if: both calls still have merge peer/host relationships configured, both sessions are not waiting to be merged into the conference, and the transient conference session is alive in the case of an initial conference.

return
{@code true} where the host and peer sessions have finished merging into the conference, {@code false} if the merge has not yet completed, and {@code false} if there is no conference merge in progress.

        boolean areMergeTriggersDone = false;

        synchronized (ImsCall.this) {
            // if there is a merge going on, then the merge host/peer relationships should have been
            // set up.  This works for both the initial conference or merging a call into an
            // existing conference.
            if (!isMergeHost() && !isMergePeer()) {
                if (VDBG) {
                    log("shouldProcessConferenceResult :: no merge in progress");
                }
                return false;
            }

            // There is a merge in progress, so check the sessions to ensure:
            // 1. Both calls have completed being merged (or failing to merge) into the conference.
            // 2. The transient conference session is alive.
            if (isMergeHost()) {
                if (VDBG) {
                    log("shouldProcessConferenceResult :: We are a merge host=" + this);
                    log("shouldProcessConferenceResult :: Here is the merge peer=" + mMergePeer);
                }
                areMergeTriggersDone = !isCallSessionMergePending() &&
                        !mMergePeer.isCallSessionMergePending();
                if (!isMultiparty()) {
                    // Only check the transient session when there is no existing conference
                    areMergeTriggersDone &= isSessionAlive(mTransientConferenceSession);
                }
            } else if (isMergePeer()) {
                if (VDBG) {
                    log("shouldProcessConferenceResult :: We are a merge peer=" + this);
                    log("shouldProcessConferenceResult :: Here is the merge host=" + mMergeHost);
                }
                areMergeTriggersDone = !isCallSessionMergePending() &&
                        !mMergeHost.isCallSessionMergePending();
                if (!mMergeHost.isMultiparty()) {
                    // Only check the transient session when there is no existing conference
                    areMergeTriggersDone &= isSessionAlive(mMergeHost.mTransientConferenceSession);
                } else {
                    // This else block is a special case for Verizon to handle these steps
                    // 1. Establish a conference call.
                    // 2. Add a new call (conference in in BG)
                    // 3. Swap (conference active on FG)
                    // 4. Merge
                    // What happens here is that the BG call gets a terminated callback
                    // because it was added to the conference. I've seen where
                    // the FG gets no callback at all because its already active.
                    // So if we continue to wait for it to set its isCallSessionMerging
                    // flag to false...we'll be waiting forever.
                    areMergeTriggersDone = !isCallSessionMergePending();
                }
            } else {
                // Realistically this shouldn't happen, but best to be safe.
                loge("shouldProcessConferenceResult : merge in progress but call is neither" +
                        "host nor peer.");
            }
            if (VDBG) {
                log("shouldProcessConferenceResult :: returning:" +
                        (areMergeTriggersDone ? "true" : "false"));
            }
        }
        return areMergeTriggersDone;
    
public voidstart(com.android.ims.internal.ImsCallSession session, java.lang.String callee)
Initiates an IMS call with the call profile which is provided when creating a {@link ImsCall}.

param
session the {@link ImsCallSession} for carrying out the call
param
callee callee information to initiate an IMS call
throws
ImsException if the IMS service fails to initiate the call

        if (DBG) {
            log("start(1) :: session=" + session + ", callee=" + callee);
        }

        synchronized(mLockObj) {
            mSession = session;

            try {
                session.setListener(createCallSessionListener());
                session.start(callee, mCallProfile);
            } catch (Throwable t) {
                loge("start(1) :: ", t);
                throw new ImsException("start(1)", t, 0);
            }
        }
    
public voidstart(com.android.ims.internal.ImsCallSession session, java.lang.String[] participants)
Initiates an IMS conferenca call with the call profile which is provided when creating a {@link ImsCall}.

param
session the {@link ImsCallSession} for carrying out the call
param
participants participant list to initiate an IMS conference call
throws
ImsException if the IMS service fails to initiate the call

        if (DBG) {
            log("start(n) :: session=" + session + ", callee=" + participants);
        }

        synchronized(mLockObj) {
            mSession = session;

            try {
                session.setListener(createCallSessionListener());
                session.start(participants, mCallProfile);
            } catch (Throwable t) {
                loge("start(n) :: ", t);
                throw new ImsException("start(n)", t, 0);
            }
        }
    
public voidstartDtmf(char c)
Start a DTMF code. According to RFC 2833, event 0 ~ 9 maps to decimal value 0 ~ 9, '*' to 10, '#' to 11, event 'A' ~ 'D' to 12 ~ 15, and event flash to 16. Currently, event flash is not supported.

param
c that represents the DTMF to send. '0' ~ '9', 'A' ~ 'D', '*', '#' are valid inputs.

        if (DBG) {
            log("startDtmf :: session=" + mSession + ", code=" + c);
        }

        synchronized(mLockObj) {
            if (mSession != null) {
                mSession.startDtmf(c);
            }
        }
    
public voidstopDtmf()
Stop a DTMF code.

        if (DBG) {
            log("stopDtmf :: session=" + mSession);
        }

        synchronized(mLockObj) {
            if (mSession != null) {
                mSession.stopDtmf();
            }
        }
    
public voidterminate(int reason)
Terminates an IMS call.

param
reason reason code to terminate a call
throws
ImsException if the IMS service fails to terminate the call

        if (VDBG) {
            log("terminate :: ImsCall=" + this +" reason=" + reason);
        }

        synchronized(mLockObj) {
            mHold = false;
            mInCall = false;

            if (mSession != null) {
                // TODO: Fix the fact that user invoked call terminations during
                // the process of establishing a conference call needs to be handled
                // as a special case.
                // Currently, any terminations (both invoked by the user or
                // by the network results in a callSessionTerminated() callback
                // from the network.  When establishing a conference call we bury
                // these callbacks until we get closure on all participants of the
                // conference. In some situations, we will throw away the callback
                // (when the underlying session of the host of the new conference
                // is terminated) or will will unbury it when the conference has been
                // established, like when the peer of the new conference goes away
                // after the conference has been created.  The UI relies on the callback
                // to reflect the fact that the call is gone.
                // So if a user decides to terminated a call while it is merging, it
                // could take a long time to reflect in the UI due to the conference
                // processing but we should probably cancel that and just terminate
                // the call immediately and clean up.  This is not a huge issue right
                // now because we have not seen instances where establishing a
                // conference takes a long time (more than a second or two).
                mSession.terminate(reason);
            }
        }
    
private voidthrowImsException(java.lang.Throwable t, int code)

        if (t instanceof ImsException) {
            throw (ImsException) t;
        } else {
            throw new ImsException(String.valueOf(code), t, code);
        }
    
public java.lang.StringtoString()
Provides a string representation of the {@link ImsCall}. Primarily intended for use in log statements.

return
String representation of call.

        StringBuilder sb = new StringBuilder();
        sb.append("[ImsCall objId:");
        sb.append(System.identityHashCode(this));
        sb.append(" onHold:");
        sb.append(isOnHold() ? "Y" : "N");
        sb.append(" mute:");
        sb.append(isMuted() ? "Y" : "N");
        sb.append(" updateRequest:");
        sb.append(updateRequestToString(mUpdateRequest));
        sb.append(" merging:");
        sb.append(isMerging() ? "Y" : "N");
        if (isMerging()) {
            if (isMergePeer()) {
                sb.append("P");
            } else {
                sb.append("H");
            }
        }
        sb.append(" merge action pending:");
        sb.append(isCallSessionMergePending() ? "Y" : "N");
        sb.append(" merged:");
        sb.append(isMerged() ? "Y" : "N");
        sb.append(" multiParty:");
        sb.append(isMultiparty() ? "Y" : "N");
        sb.append(" buried term:");
        sb.append(mSessionEndDuringMerge ? "Y" : "N");
        sb.append(" session:");
        sb.append(mSession);
        sb.append(" transientSession:");
        sb.append(mTransientConferenceSession);
        sb.append("]");
        return sb.toString();
    
private voidtryProcessConferenceResult()
This function will determine if there is a pending conference and if we are ready to finalize processing it.

        if (shouldProcessConferenceResult()) {
            if (isMergeHost()) {
                processMergeComplete();
            } else if (mMergeHost != null) {
                mMergeHost.processMergeComplete();
            } else {
                // There must be a merge host at this point.
                loge("tryProcessConferenceResult :: No merge host for this conference!");
            }
        }
    
public voidupdate(int callType, ImsStreamMediaProfile mediaProfile)
Updates the current call's properties (ex. call mode change: video upgrade / downgrade).

        if (VDBG) {
            log("update ::");
        }

        if (isOnHold()) {
            if (DBG) {
                log("update :: call is on hold");
            }
            throw new ImsException("Not in a call to update call",
                    ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE);
        }

        synchronized(mLockObj) {
            if (mUpdateRequest != UPDATE_NONE) {
                if (DBG) {
                    log("update :: update is in progress; request=" +
                            updateRequestToString(mUpdateRequest));
                }
                throw new ImsException("Call update is in progress",
                        ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE);
            }

            if (mSession == null) {
                loge("update :: ");
                throw new ImsException("No call session",
                        ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
            }

            mSession.update(callType, mediaProfile);
            mUpdateRequest = UPDATE_UNSPECIFIED;
        }
    
private java.lang.StringupdateRequestToString(int updateRequest)
Provides a human-readable string representation of an update request.

param
updateRequest The update request.
return
The string representation.

        switch (updateRequest) {
            case UPDATE_NONE:
                return "NONE";
            case UPDATE_HOLD:
                return "HOLD";
            case UPDATE_HOLD_MERGE:
                return "HOLD_MERGE";
            case UPDATE_RESUME:
                return "RESUME";
            case UPDATE_MERGE:
                return "MERGE";
            case UPDATE_EXTEND_TO_CONFERENCE:
                return "EXTEND_TO_CONFERENCE";
            case UPDATE_UNSPECIFIED:
                return "UNSPECIFIED";
            default:
                return "UNKNOWN";
        }