/* * Copyright (c) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ims; import com.android.internal.R; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.Set; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.os.Message; import android.telecom.ConferenceParticipant; import android.util.Log; import com.android.ims.internal.ICall; import com.android.ims.internal.ImsCallSession; import com.android.ims.internal.ImsStreamMediaSession; import com.android.internal.annotations.VisibleForTesting; /** * Handles an IMS voice / video call over LTE. You can instantiate this class with * {@link ImsManager}. * * @hide */ public class ImsCall implements ICall { // Mode of USSD message public static final int USSD_MODE_NOTIFY = 0; public static final int USSD_MODE_REQUEST = 1; private static final String TAG = "ImsCall"; // This flag is meant to be used as a debugging tool to quickly see all logs // regardless of the actual log level set on this component. private static final boolean FORCE_DEBUG = false; /* STOPSHIP if true */ // We will log messages guarded by these flags at the info level. If logging is required // to occur at (and only at) a particular log level, please use the logd, logv and loge // functions as those will not be affected by the value of FORCE_DEBUG at all. // Otherwise, anything guarded by these flags will be logged at the info level since that // level allows those statements ot be logged by default which supports the workflow of // setting FORCE_DEBUG and knowing these logs will show up regardless of the actual log // level of this component. private static final boolean DBG = FORCE_DEBUG || Log.isLoggable(TAG, Log.DEBUG); private static final boolean VDBG = FORCE_DEBUG || Log.isLoggable(TAG, Log.VERBOSE); // This is a special flag that is used only to highlight specific log around bringing // up and tearing down conference calls. At times, these errors are transient and hard to // reproduce so we need to capture this information the first time. // TODO: Set this flag to FORCE_DEBUG once the new conference call logic gets more mileage // across different IMS implementations. private static final boolean CONF_DBG = true; /** * Listener for events relating to an IMS call, such as when a call is being * received ("on ringing") or a call is outgoing ("on calling"). *
Many of these events are also received by {@link ImsCallSession.Listener}.
*/ public static class Listener { /** * Called when a request is sent out to initiate a new call * and 1xx response is received from the network. * The default implementation calls {@link #onCallStateChanged}. * * @param call the call object that carries out the IMS call */ public void onCallProgressing(ImsCall call) { onCallStateChanged(call); } /** * Called when the call is established. * The default implementation calls {@link #onCallStateChanged}. * * @param call the call object that carries out the IMS call */ public void onCallStarted(ImsCall call) { onCallStateChanged(call); } /** * Called when the call setup is failed. * The default implementation calls {@link #onCallError}. * * @param call the call object that carries out the IMS call * @param reasonInfo detailed reason of the call setup failure */ public void onCallStartFailed(ImsCall call, ImsReasonInfo reasonInfo) { onCallError(call, reasonInfo); } /** * Called when the call is terminated. * The default implementation calls {@link #onCallStateChanged}. * * @param call the call object that carries out the IMS call * @param reasonInfo detailed reason of the call termination */ public void onCallTerminated(ImsCall call, ImsReasonInfo reasonInfo) { // Store the call termination reason onCallStateChanged(call); } /** * Called when the call is in hold. * The default implementation calls {@link #onCallStateChanged}. * * @param call the call object that carries out the IMS call */ public void onCallHeld(ImsCall call) { onCallStateChanged(call); } /** * Called when the call hold is failed. * The default implementation calls {@link #onCallError}. * * @param call the call object that carries out the IMS call * @param reasonInfo detailed reason of the call hold failure */ public void onCallHoldFailed(ImsCall call, ImsReasonInfo reasonInfo) { onCallError(call, reasonInfo); } /** * Called when the call hold is received from the remote user. * The default implementation calls {@link #onCallStateChanged}. * * @param call the call object that carries out the IMS call */ public void onCallHoldReceived(ImsCall call) { onCallStateChanged(call); } /** * Called when the call is in call. * The default implementation calls {@link #onCallStateChanged}. * * @param call the call object that carries out the IMS call */ public void onCallResumed(ImsCall call) { onCallStateChanged(call); } /** * Called when the call resume is failed. * The default implementation calls {@link #onCallError}. * * @param call the call object that carries out the IMS call * @param reasonInfo detailed reason of the call resume failure */ public void onCallResumeFailed(ImsCall call, ImsReasonInfo reasonInfo) { onCallError(call, reasonInfo); } /** * Called when the call resume is received from the remote user. * The default implementation calls {@link #onCallStateChanged}. * * @param call the call object that carries out the IMS call */ public void onCallResumeReceived(ImsCall call) { onCallStateChanged(call); } /** * Called when the call is in call. * The default implementation calls {@link #onCallStateChanged}. * * @param call the call object that carries out the IMS call * @param swapCalls {@code true} if the foreground and background calls should be swapped * now that the merge has completed. */ public void onCallMerged(ImsCall call, boolean swapCalls) { onCallStateChanged(call); } /** * Called when the call merge is failed. * The default implementation calls {@link #onCallError}. * * @param call the call object that carries out the IMS call * @param reasonInfo detailed reason of the call merge failure */ public void onCallMergeFailed(ImsCall call, ImsReasonInfo reasonInfo) { onCallError(call, reasonInfo); } /** * Called when the call is updated (except for hold/unhold). * The default implementation calls {@link #onCallStateChanged}. * * @param call the call object that carries out the IMS call */ public void onCallUpdated(ImsCall call) { onCallStateChanged(call); } /** * Called when the call update is failed. * The default implementation calls {@link #onCallError}. * * @param call the call object that carries out the IMS call * @param reasonInfo detailed reason of the call update failure */ public void onCallUpdateFailed(ImsCall call, ImsReasonInfo reasonInfo) { onCallError(call, reasonInfo); } /** * Called when the call update is received from the remote user. * * @param call the call object that carries out the IMS call */ public void onCallUpdateReceived(ImsCall call) { // no-op } /** * Called when the call is extended to the conference call. * The default implementation calls {@link #onCallStateChanged}. * * @param call the call object that carries out the IMS call * @param newCall the call object that is extended to the conference from the active call */ public void onCallConferenceExtended(ImsCall call, ImsCall newCall) { onCallStateChanged(call); } /** * Called when the conference extension is failed. * The default implementation calls {@link #onCallError}. * * @param call the call object that carries out the IMS call * @param reasonInfo detailed reason of the conference extension failure */ public void onCallConferenceExtendFailed(ImsCall call, ImsReasonInfo reasonInfo) { onCallError(call, reasonInfo); } /** * Called when the conference extension is received from the remote user. * * @param call the call object that carries out the IMS call * @param newCall the call object that is extended to the conference from the active call */ public void onCallConferenceExtendReceived(ImsCall call, ImsCall newCall) { onCallStateChanged(call); } /** * Called when the invitation request of the participants is delivered to * the conference server. * * @param call the call object that carries out the IMS call */ public void onCallInviteParticipantsRequestDelivered(ImsCall call) { // no-op } /** * Called when the invitation request of the participants is failed. * * @param call the call object that carries out the IMS call * @param reasonInfo detailed reason of the conference invitation failure */ public void onCallInviteParticipantsRequestFailed(ImsCall call, ImsReasonInfo reasonInfo) { // no-op } /** * Called when the removal request of the participants is delivered to * the conference server. * * @param call the call object that carries out the IMS call */ public void onCallRemoveParticipantsRequestDelivered(ImsCall call) { // no-op } /** * Called when the removal request of the participants is failed. * * @param call the call object that carries out the IMS call * @param reasonInfo detailed reason of the conference removal failure */ public void onCallRemoveParticipantsRequestFailed(ImsCall call, ImsReasonInfo reasonInfo) { // no-op } /** * Called when the conference state is updated. * * @param call the call object that carries out the IMS call * @param state state of the participant who is participated in the conference call */ public void onCallConferenceStateUpdated(ImsCall call, ImsConferenceState state) { // no-op } /** * Called when the state of IMS conference participant(s) has changed. * * @param call the call object that carries out the IMS call. * @param participants the participant(s) and their new state information. */ public void onConferenceParticipantsStateChanged(ImsCall call, List
* When {@code true}, this {@link ImsCall} is is the origin of the conference call.
* When {@code false}, this {@link ImsCall} is a member of a conference started on another
* device.
*/
private boolean mIsConferenceHost = false;
/**
* Create an IMS call object.
*
* @param context the context for accessing system services
* @param profile the call profile to make/take a call
*/
public ImsCall(Context context, ImsCallProfile profile) {
mContext = context;
mCallProfile = profile;
}
/**
* Closes this object. This object is not usable after being closed.
*/
@Override
public void close() {
synchronized(mLockObj) {
if (mSession != null) {
mSession.close();
mSession = null;
}
mCallProfile = null;
mProposedCallProfile = null;
mLastReasonInfo = null;
mMediaSession = null;
}
}
/**
* 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
*/
@Override
public boolean checkIfRemoteUserIsSame(String userId) {
if (userId == null) {
return false;
}
return userId.equals(mCallProfile.getCallExtra(ImsCallProfile.EXTRA_REMOTE_URI, ""));
}
/**
* Checks if the call is equal or not.
*
* @param call the call to be compared
* @return true if the call is equal; otherwise, false
*/
@Override
public boolean equalsTo(ICall call) {
if (call == null) {
return false;
}
if (call instanceof ImsCall) {
return this.equals(call);
}
return false;
}
public static boolean isSessionAlive(ImsCallSession session) {
return session != null && session.isAlive();
}
/**
* Gets the negotiated (local & remote) call profile.
*
* @return a {@link ImsCallProfile} object that has the negotiated call profile
*/
public ImsCallProfile getCallProfile() {
synchronized(mLockObj) {
return mCallProfile;
}
}
/**
* Gets the local call profile (local capabilities).
*
* @return a {@link ImsCallProfile} object that has the local call profile
*/
public ImsCallProfile getLocalCallProfile() throws ImsException {
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);
}
}
}
/**
* Gets the remote call profile (remote capabilities).
*
* @return a {@link ImsCallProfile} object that has the remote call profile
*/
public ImsCallProfile getRemoteCallProfile() throws ImsException {
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);
}
}
}
/**
* Gets the call profile proposed by the local/remote user.
*
* @return a {@link ImsCallProfile} object that has the proposed call profile
*/
public ImsCallProfile getProposedCallProfile() {
synchronized(mLockObj) {
if (!isInCall()) {
return null;
}
return mProposedCallProfile;
}
}
/**
* 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
*/
public int getState() {
synchronized(mLockObj) {
if (mSession == null) {
return ImsCallSession.State.IDLE;
}
return mSession.getState();
}
}
/**
* Gets the {@link ImsCallSession} that carries this call.
*
* @return the session object that carries this call
* @hide
*/
public ImsCallSession getCallSession() {
synchronized(mLockObj) {
return mSession;
}
}
/**
* 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
*/
public ImsStreamMediaSession getMediaSession() {
synchronized(mLockObj) {
return mMediaSession;
}
}
/**
* 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
*/
public String getCallExtra(String name) throws ImsException {
// 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);
}
}
}
/**
* Gets the last reason information when the call is not established, cancelled or terminated.
*
* @return the last reason information
*/
public ImsReasonInfo getLastReasonInfo() {
synchronized(mLockObj) {
return mLastReasonInfo;
}
}
/**
* Checks if the call has a pending update operation.
*
* @return true if the call has a pending update operation
*/
public boolean hasPendingUpdate() {
synchronized(mLockObj) {
return (mUpdateRequest != UPDATE_NONE);
}
}
/**
* Checks if the call is established.
*
* @return true if the call is established
*/
public boolean isInCall() {
synchronized(mLockObj) {
return mInCall;
}
}
/**
* Checks if the call is muted.
*
* @return true if the call is muted
*/
public boolean isMuted() {
synchronized(mLockObj) {
return mMute;
}
}
/**
* Checks if the call is on hold.
*
* @return true if the call is on hold
*/
public boolean isOnHold() {
synchronized(mLockObj) {
return mHold;
}
}
/**
* Determines if the call is a multiparty call.
*
* @return {@code True} if the call is a multiparty call.
*/
public boolean isMultiparty() {
synchronized(mLockObj) {
if (mSession == null) {
return false;
}
return mSession.isMultiparty();
}
}
/**
* Where {@link #isMultiparty()} is {@code true}, determines if this {@link ImsCall} is the
* origin of the conference call (i.e. {@code #isConferenceHost()} is {@code true}), or if this
* {@link ImsCall} is a member of a conference hosted on another device.
*
* @return {@code true} if this call is the origin of the conference call it is a member of,
* {@code false} otherwise.
*/
public boolean isConferenceHost() {
synchronized(mLockObj) {
return isMultiparty() && mIsConferenceHost;
}
}
/**
* 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.
*/
public void setIsMerged(boolean isMerged) {
mIsMerged = isMerged;
}
/**
* @return {@code true} if the call recently merged into a conference call.
*/
public boolean isMerged() {
return mIsMerged;
}
/**
* 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)
*/
public void setListener(ImsCall.Listener listener) {
setListener(listener, false);
}
/**
* 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
*/
public void setListener(ImsCall.Listener listener, boolean callbackImmediately) {
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);
}
}
/**
* Mutes or unmutes the mic for the active call.
*
* @param muted true if the call is muted, false otherwise
*/
public void setMute(boolean muted) throws ImsException {
synchronized(mLockObj) {
if (mMute != muted) {
logi("setMute :: turning mute " + (muted ? "on" : "off"));
mMute = muted;
try {
mSession.setMute(muted);
} catch (Throwable t) {
loge("setMute :: ", t);
throwImsException(t, 0);
}
}
}
}
/**
* 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
*/
public void attachSession(ImsCallSession session) throws ImsException {
logi("attachSession :: session=" + session);
synchronized(mLockObj) {
mSession = session;
try {
mSession.setListener(createCallSessionListener());
} catch (Throwable t) {
loge("attachSession :: ", t);
throwImsException(t, 0);
}
}
}
/**
* 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
*/
public void start(ImsCallSession session, String callee)
throws ImsException {
logi("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);
}
}
}
/**
* 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
*/
public void start(ImsCallSession session, String[] participants)
throws ImsException {
logi("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);
}
}
}
/**
* 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
*/
public void accept(int callType) throws ImsException {
accept(callType, new ImsStreamMediaProfile());
}
/**
* 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
*/
public void accept(int callType, ImsStreamMediaProfile profile) throws ImsException {
logi("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) {
logi("accept :: call profile will be updated");
}
mCallProfile = mProposedCallProfile;
mProposedCallProfile = null;
}
// Other call update received
if (mInCall && (mUpdateRequest == UPDATE_UNSPECIFIED)) {
mUpdateRequest = UPDATE_NONE;
}
}
}
/**
* Rejects a call.
*
* @param reason reason code to reject an incoming call
* @see Listener#onCallStartFailed
* @throws ImsException if the IMS service fails to reject the call
*/
public void reject(int reason) throws ImsException {
logi("reject :: reason=" + reason);
synchronized(mLockObj) {
if (mSession != null) {
mSession.reject(reason);
}
if (mInCall && (mProposedCallProfile != null)) {
if (DBG) {
logi("reject :: call profile is not updated; destroy it...");
}
mProposedCallProfile = null;
}
// Other call update received
if (mInCall && (mUpdateRequest == UPDATE_UNSPECIFIED)) {
mUpdateRequest = UPDATE_NONE;
}
}
}
/**
* Terminates an IMS call.
*
* @param reason reason code to terminate a call
* @throws ImsException if the IMS service fails to terminate the call
*/
public void terminate(int reason) throws ImsException {
logi("terminate :: 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);
}
}
}
/**
* 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
*/
public void hold() throws ImsException {
logi("hold :: ");
if (isOnHold()) {
if (DBG) {
logi("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) {
throw new ImsException("No call session",
ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
}
mSession.hold(createHoldMediaProfile());
// FIXME: We should update the state on the callback because that is where
// we can confirm that the hold request was successful or not.
mHold = true;
mUpdateRequest = UPDATE_HOLD;
}
}
/**
* 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
*/
public void resume() throws ImsException {
logi("resume :: ");
if (!isOnHold()) {
if (DBG) {
logi("resume :: call is not being held");
}
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());
}
}
/**
* Merges the active & hold call.
*
* @see Listener#onCallMerged, Listener#onCallMergeFailed
* @throws ImsException if the IMS service fails to merge the call
*/
private void merge() throws ImsException {
logi("merge :: ");
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;
}
}
}
/**
* 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
*/
public void merge(ImsCall bgCall) throws ImsException {
logi("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();
}
/**
* Updates the current call's properties (ex. call mode change: video upgrade / downgrade).
*/
public void update(int callType, ImsStreamMediaProfile mediaProfile) throws ImsException {
logi("update :: callType=" + callType + ", mediaProfile=" + mediaProfile);
if (isOnHold()) {
if (DBG) {
logi("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) {
logi("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;
}
}
/**
* Extends this call (1-to-1 call) to the conference call
* inviting the specified participants to.
*
*/
public void extendToConference(String[] participants) throws ImsException {
logi("extendToConference ::");
if (isOnHold()) {
if (DBG) {
logi("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 (CONF_DBG) {
logi("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;
}
}
/**
* Requests the conference server to invite an additional participants to the conference.
*
*/
public void inviteParticipants(String[] participants) throws ImsException {
logi("inviteParticipants ::");
synchronized(mLockObj) {
if (mSession == null) {
loge("inviteParticipants :: ");
throw new ImsException("No call session",
ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
}
mSession.inviteParticipants(participants);
}
}
/**
* Requests the conference server to remove the specified participants from the conference.
*
*/
public void removeParticipants(String[] participants) throws ImsException {
logi("removeParticipants ::");
synchronized(mLockObj) {
if (mSession == null) {
loge("removeParticipants :: ");
throw new ImsException("No call session",
ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
}
mSession.removeParticipants(participants);
}
}
/**
* 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.
*/
public void sendDtmf(char c, Message result) {
logi("sendDtmf :: code=" + c);
synchronized(mLockObj) {
if (mSession != null) {
mSession.sendDtmf(c, result);
}
}
}
/**
* 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.
*/
public void startDtmf(char c) {
logi("startDtmf :: code=" + c);
synchronized(mLockObj) {
if (mSession != null) {
mSession.startDtmf(c);
}
}
}
/**
* Stop a DTMF code.
*/
public void stopDtmf() {
logi("stopDtmf :: ");
synchronized(mLockObj) {
if (mSession != null) {
mSession.stopDtmf();
}
}
}
/**
* Sends an USSD message.
*
* @param ussdMessage USSD message to send
*/
public void sendUssd(String ussdMessage) throws ImsException {
logi("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 void clear(ImsReasonInfo lastReasonInfo) {
mInCall = false;
mHold = false;
mUpdateRequest = UPDATE_NONE;
mLastReasonInfo = lastReasonInfo;
}
/**
* Creates an IMS call session listener.
*/
private ImsCallSession.Listener createCallSessionListener() {
return new ImsCallSessionListenerProxy();
}
private ImsCall createNewCall(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 ImsStreamMediaProfile createHoldMediaProfile() {
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 ImsStreamMediaProfile createResumeMediaProfile() {
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 void enforceConversationMode() {
if (mInCall) {
mHold = false;
mUpdateRequest = UPDATE_NONE;
}
}
private void mergeInternal() {
if (CONF_DBG) {
logi("mergeInternal :: ");
}
mSession.merge();
mUpdateRequest = UPDATE_MERGE;
}
private void notifyConferenceSessionTerminated(ImsReasonInfo reasonInfo) {
ImsCall.Listener listener = mListener;
clear(reasonInfo);
if (listener != null) {
try {
listener.onCallTerminated(this, reasonInfo);
} catch (Throwable t) {
loge("notifyConferenceSessionTerminated :: ", t);
}
}
}
private void notifyConferenceStateUpdated(ImsConferenceState state) {
Set
* 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.
*/
private boolean shouldProcessConferenceResult() {
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 (CONF_DBG) {
loge("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 (CONF_DBG) {
logi("shouldProcessConferenceResult :: We are a merge host");
logi("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 (CONF_DBG) {
logi("shouldProcessConferenceResult :: We are a merge peer");
logi("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 (CONF_DBG) {
logi("shouldProcessConferenceResult :: returning:" +
(areMergeTriggersDone ? "true" : "false"));
}
}
return areMergeTriggersDone;
}
/**
* Provides a string representation of the {@link ImsCall}. Primarily intended for use in log
* statements.
*
* @return String representation of call.
*/
@Override
public String toString() {
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(" confHost:");
sb.append(isConferenceHost() ? "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 void throwImsException(Throwable t, int code) throws ImsException {
if (t instanceof ImsException) {
throw (ImsException) t;
} else {
throw new ImsException(String.valueOf(code), t, code);
}
}
/**
* Append the ImsCall information to the provided string. Usefull for as a logging helper.
* @param s The original string
* @return The original string with {@code ImsCall} information appended to it.
*/
private String appendImsCallInfoToString(String s) {
StringBuilder sb = new StringBuilder();
sb.append(s);
sb.append(" ImsCall=");
sb.append(ImsCall.this);
return sb.toString();
}
/**
* Log a string to the radio buffer at the info level.
* @param s The message to log
*/
private void logi(String s) {
Log.i(TAG, appendImsCallInfoToString(s));
}
/**
* Log a string to the radio buffer at the debug level.
* @param s The message to log
*/
private void logd(String s) {
Log.d(TAG, appendImsCallInfoToString(s));
}
/**
* Log a string to the radio buffer at the verbose level.
* @param s The message to log
*/
private void logv(String s) {
Log.v(TAG, appendImsCallInfoToString(s));
}
/**
* Log a string to the radio buffer at the error level.
* @param s The message to log
*/
private void loge(String s) {
Log.e(TAG, appendImsCallInfoToString(s));
}
/**
* Log a string to the radio buffer at the error level with a throwable
* @param s The message to log
* @param t The associated throwable
*/
private void loge(String s, Throwable t) {
Log.e(TAG, appendImsCallInfoToString(s), t);
}
}