3989 lines
147 KiB
Java
3989 lines
147 KiB
Java
/*
|
|
* 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 android.annotation.NonNull;
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.content.Context;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.os.Message;
|
|
import android.os.Parcel;
|
|
import android.telecom.Call;
|
|
import com.android.ims.internal.ConferenceParticipant;
|
|
import android.telecom.Connection;
|
|
import android.telephony.CallQuality;
|
|
import android.telephony.ServiceState;
|
|
import android.telephony.TelephonyManager;
|
|
import android.telephony.ims.ImsCallProfile;
|
|
import android.telephony.ims.ImsCallSession;
|
|
import android.telephony.ims.ImsCallSessionListener;
|
|
import android.telephony.ims.ImsConferenceState;
|
|
import android.telephony.ims.ImsReasonInfo;
|
|
import android.telephony.ims.ImsStreamMediaProfile;
|
|
import android.telephony.ims.ImsSuppServiceNotification;
|
|
import android.telephony.ims.RtpHeaderExtension;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import com.android.ims.internal.ICall;
|
|
import com.android.ims.internal.ImsStreamMediaSession;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.telephony.Rlog;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Map.Entry;
|
|
import java.util.Set;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
private List<ConferenceParticipant> mConferenceParticipants;
|
|
/**
|
|
* 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").
|
|
* <p>Many of these events are also received by {@link ImsCallSession.Listener}.</p>
|
|
*/
|
|
public static class Listener {
|
|
/**
|
|
* Called after the network first begins to establish the call session and is now connecting
|
|
* to the remote party.
|
|
* The default implementation calls {@link #onCallStateChanged}.
|
|
* <p/>
|
|
* see: {@link ImsCallSessionListener#callSessionInitiating}
|
|
*/
|
|
public void onCallInitiating(ImsCall call) {
|
|
onCallStateChanged(call);
|
|
}
|
|
|
|
/**
|
|
* Called after the network has contacted the remote party.
|
|
* The default implementation calls {@link #onCallStateChanged}.
|
|
* <p/>
|
|
* see: {@link ImsCallSessionListener#callSessionProgressing}
|
|
*/
|
|
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 active IMS call
|
|
* @param peerCall the call object that carries out the held 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, ImsCall peerCall, 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<ConferenceParticipant> participants) {
|
|
// no-op
|
|
}
|
|
|
|
/**
|
|
* Called when the USSD message is received from the network.
|
|
*
|
|
* @param mode mode of the USSD message (REQUEST / NOTIFY)
|
|
* @param ussdMessage USSD message
|
|
*/
|
|
public void onCallUssdMessageReceived(ImsCall call,
|
|
int mode, String ussdMessage) {
|
|
// no-op
|
|
}
|
|
|
|
/**
|
|
* Called when an error occurs. The default implementation is no op.
|
|
* overridden. The default implementation is no op. Error events are
|
|
* not re-directed to this callback and are handled in {@link #onCallError}.
|
|
*
|
|
* @param call the call object that carries out the IMS call
|
|
* @param reasonInfo detailed reason of this error
|
|
* @see ImsReasonInfo
|
|
*/
|
|
public void onCallError(ImsCall call, ImsReasonInfo reasonInfo) {
|
|
// no-op
|
|
}
|
|
|
|
/**
|
|
* Called when an event occurs and the corresponding callback is not
|
|
* overridden. The default implementation is no op. Error events are
|
|
* not re-directed to this callback and are handled in {@link #onCallError}.
|
|
*
|
|
* @param call the call object that carries out the IMS call
|
|
*/
|
|
public void onCallStateChanged(ImsCall call) {
|
|
// no-op
|
|
}
|
|
|
|
/**
|
|
* Called when the call moves the hold state to the conversation state.
|
|
* For example, when merging the active & hold call, the state of all the hold call
|
|
* will be changed from hold state to conversation state.
|
|
* This callback method can be invoked even though the application does not trigger
|
|
* any operations.
|
|
*
|
|
* @param call the call object that carries out the IMS call
|
|
* @param state the detailed state of call state changes;
|
|
* Refer to CALL_STATE_* in {@link ImsCall}
|
|
*/
|
|
public void onCallStateChanged(ImsCall call, int state) {
|
|
// no-op
|
|
}
|
|
|
|
/**
|
|
* Called when the call supp service is received
|
|
* The default implementation calls {@link #onCallStateChanged}.
|
|
*
|
|
* @param call the call object that carries out the IMS call
|
|
*/
|
|
public void onCallSuppServiceReceived(ImsCall call,
|
|
ImsSuppServiceNotification suppServiceInfo) {
|
|
}
|
|
|
|
/**
|
|
* Called when TTY mode of remote party changed
|
|
*
|
|
* @param call the call object that carries out the IMS call
|
|
* @param mode TTY mode of remote party
|
|
*/
|
|
public void onCallSessionTtyModeReceived(ImsCall call, int mode) {
|
|
// no-op
|
|
}
|
|
|
|
/**
|
|
* Called when handover occurs from one access technology to another.
|
|
*
|
|
* @param imsCall ImsCall object
|
|
* @param srcAccessTech original access technology
|
|
* @param targetAccessTech new access technology
|
|
* @param reasonInfo
|
|
*/
|
|
public void onCallHandover(ImsCall imsCall, int srcAccessTech, int targetAccessTech,
|
|
ImsReasonInfo reasonInfo) {
|
|
}
|
|
|
|
/**
|
|
* Called when the remote party issues an RTT modify request
|
|
*
|
|
* @param imsCall ImsCall object
|
|
*/
|
|
public void onRttModifyRequestReceived(ImsCall imsCall) {
|
|
}
|
|
|
|
/**
|
|
* Called when the remote party responds to a locally-issued RTT request.
|
|
*
|
|
* @param imsCall ImsCall object
|
|
* @param status The status of the request. See
|
|
* {@link Connection.RttModifyStatus} for possible values.
|
|
*/
|
|
public void onRttModifyResponseReceived(ImsCall imsCall, int status) {
|
|
}
|
|
|
|
/**
|
|
* Called when the remote party has sent some characters via RTT
|
|
*
|
|
* @param imsCall ImsCall object
|
|
* @param message A string containing the transmitted characters.
|
|
*/
|
|
public void onRttMessageReceived(ImsCall imsCall, String message) {
|
|
}
|
|
|
|
/**
|
|
* Called when handover from one access technology to another fails.
|
|
*
|
|
* @param imsCall call that failed the handover.
|
|
* @param srcAccessTech original access technology
|
|
* @param targetAccessTech new access technology
|
|
* @param reasonInfo
|
|
*/
|
|
public void onCallHandoverFailed(ImsCall imsCall, int srcAccessTech, int targetAccessTech,
|
|
ImsReasonInfo reasonInfo) {
|
|
}
|
|
|
|
/**
|
|
* Notifies of a change to the multiparty state for this {@code ImsCall}.
|
|
*
|
|
* @param imsCall The IMS call.
|
|
* @param isMultiParty {@code true} if the call became multiparty, {@code false}
|
|
* otherwise.
|
|
*/
|
|
public void onMultipartyStateChanged(ImsCall imsCall, boolean isMultiParty) {
|
|
}
|
|
|
|
/**
|
|
* Called when rtt call audio indicator has changed.
|
|
*
|
|
* @param imsCall ImsCall object
|
|
* @param profile updated ImsStreamMediaProfile profile.
|
|
*/
|
|
public void onRttAudioIndicatorChanged(ImsCall imsCall, ImsStreamMediaProfile profile) {
|
|
}
|
|
|
|
/**
|
|
* Notifies the result of transfer request.
|
|
*
|
|
* @param imsCall ImsCall object
|
|
*/
|
|
public void onCallSessionTransferred(ImsCall imsCall) {
|
|
}
|
|
|
|
public void onCallSessionTransferFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) {
|
|
}
|
|
|
|
/**
|
|
* Reports a DTMF tone received from the network.
|
|
* @param imsCall The IMS call the tone was received from.
|
|
* @param digit The digit received.
|
|
*/
|
|
public void onCallSessionDtmfReceived(ImsCall imsCall, char digit) {
|
|
}
|
|
|
|
/**
|
|
* Called when the call quality has changed.
|
|
*
|
|
* @param imsCall ImsCall object
|
|
* @param callQuality the updated CallQuality
|
|
*/
|
|
public void onCallQualityChanged(ImsCall imsCall, CallQuality callQuality) {
|
|
}
|
|
|
|
/**
|
|
* Called when RTP header extension data is received from the network.
|
|
* @param imsCall The ImsCall the data was received on.
|
|
* @param rtpHeaderExtensionData The RTP extension data received.
|
|
*/
|
|
public void onCallSessionRtpHeaderExtensionsReceived(ImsCall imsCall,
|
|
@NonNull Set<RtpHeaderExtension> rtpHeaderExtensionData) {
|
|
}
|
|
|
|
/**
|
|
* Access Network Bitrate Recommendation Query (ANBRQ), see 3GPP TS 26.114.
|
|
* This API triggers radio to send ANBRQ message to the access network to query the
|
|
* desired bitrate.
|
|
*
|
|
* @param imsCall The ImsCall the data was received on.
|
|
* @param mediaType MediaType is used to identify media stream such as audio or video.
|
|
* @param direction Direction of this packet stream (e.g. uplink or downlink).
|
|
* @param bitsPerSecond This value is the bitrate requested by the other party UE through
|
|
* RTP CMR, RTCPAPP or TMMBR, and ImsStack converts this value to the MAC bitrate
|
|
* (defined in TS36.321, range: 0 ~ 8000 kbit/s).
|
|
*/
|
|
public void onCallSessionSendAnbrQuery(ImsCall imsCall, int mediaType, int direction,
|
|
int bitsPerSecond) {
|
|
}
|
|
}
|
|
|
|
// List of update operation for IMS call control
|
|
private static final int UPDATE_NONE = 0;
|
|
private static final int UPDATE_HOLD = 1;
|
|
private static final int UPDATE_HOLD_MERGE = 2;
|
|
private static final int UPDATE_RESUME = 3;
|
|
private static final int UPDATE_MERGE = 4;
|
|
private static final int UPDATE_EXTEND_TO_CONFERENCE = 5;
|
|
private static final int UPDATE_UNSPECIFIED = 6;
|
|
|
|
// For synchronization of private variables
|
|
private Object mLockObj = new Object();
|
|
private Context mContext;
|
|
|
|
// true if the call is established & in the conversation state
|
|
private boolean mInCall = false;
|
|
// true if the call is on hold
|
|
// If it is triggered by the local, mute the call. Otherwise, play local hold tone
|
|
// or network generated media.
|
|
private boolean mHold = false;
|
|
// true if the call is on mute
|
|
private boolean mMute = false;
|
|
// It contains the exclusive call update request. Refer to UPDATE_*.
|
|
private int mUpdateRequest = UPDATE_NONE;
|
|
|
|
private ImsCall.Listener mListener = null;
|
|
|
|
// When merging two calls together, the "peer" call that will merge into this call.
|
|
private ImsCall mMergePeer = null;
|
|
// When merging two calls together, the "host" call we are merging into.
|
|
private ImsCall mMergeHost = null;
|
|
|
|
// True if Conference request was initiated by
|
|
// Foreground Conference call else it will be false
|
|
private boolean mMergeRequestedByConference = false;
|
|
// Wrapper call session to interworking the IMS service (server).
|
|
private ImsCallSession mSession = null;
|
|
// Call profile of the current session.
|
|
// It can be changed at anytime when the call is updated.
|
|
private ImsCallProfile mCallProfile = null;
|
|
// Call profile to be updated after the application's action (accept/reject)
|
|
// to the call update. After the application's action (accept/reject) is done,
|
|
// it will be set to null.
|
|
private ImsCallProfile mProposedCallProfile = null;
|
|
private ImsReasonInfo mLastReasonInfo = null;
|
|
|
|
// Media session to control media (audio/video) operations for an IMS call
|
|
private ImsStreamMediaSession mMediaSession = null;
|
|
|
|
// The temporary ImsCallSession that could represent the merged call once
|
|
// we receive notification that the merge was successful.
|
|
private ImsCallSession mTransientConferenceSession = null;
|
|
// While a merge is progressing, we bury any session termination requests
|
|
// made on the original ImsCallSession until we have closure on the merge request
|
|
// If the request ultimately fails, we need to act on the termination request
|
|
// that we buried temporarily. We do this because we feel that timing issues could
|
|
// cause the termination request to occur just because the merge is succeeding.
|
|
private boolean mSessionEndDuringMerge = false;
|
|
// Just like mSessionEndDuringMerge, we need to keep track of the reason why the
|
|
// termination request was made on the original session in case we need to act
|
|
// on it in the case of a merge failure.
|
|
private ImsReasonInfo mSessionEndDuringMergeReasonInfo = null;
|
|
// This flag is used to indicate if this ImsCall was merged into a conference
|
|
// or not. It is used primarily to determine if a disconnect sound should
|
|
// be heard when the call is terminated.
|
|
private boolean mIsMerged = false;
|
|
// If true, this flag means that this ImsCall is in the process of merging
|
|
// into a conference but it does not yet have closure on if it was
|
|
// actually added to the conference or not. false implies that it either
|
|
// is not part of a merging conference or already knows if it was
|
|
// successfully added.
|
|
private boolean mCallSessionMergePending = false;
|
|
|
|
/**
|
|
* If {@code true}, this flag indicates that a request to terminate the call was made by
|
|
* Telephony (could be from the user or some internal telephony logic)
|
|
* and that when we receive a {@link #processCallTerminated(ImsReasonInfo)} callback from the
|
|
* radio indicating that the call was terminated, we should override any burying of the
|
|
* termination due to an ongoing conference merge.
|
|
*/
|
|
private boolean mTerminationRequestPending = false;
|
|
|
|
/**
|
|
* For multi-party IMS calls (e.g. conferences), determines if this {@link ImsCall} is the one
|
|
* hosting the call. This is used to distinguish between a situation where an {@link ImsCall}
|
|
* is {@link #isMultiparty()} because calls were merged on the device, and a situation where
|
|
* an {@link ImsCall} is {@link #isMultiparty()} because it is a member of a conference started
|
|
* on another device.
|
|
* <p>
|
|
* 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;
|
|
|
|
/**
|
|
* Tracks whether this {@link ImsCall} has been a video call at any point in its lifetime.
|
|
* Some examples of calls which are/were video calls:
|
|
* 1. A call which has been a video call for its duration.
|
|
* 2. An audio call upgraded to video (and potentially downgraded to audio later).
|
|
* 3. A call answered as video which was downgraded to audio.
|
|
*/
|
|
private boolean mWasVideoCall = false;
|
|
|
|
/**
|
|
* Unique id generator used to generate call id.
|
|
*/
|
|
private static final AtomicInteger sUniqueIdGenerator = new AtomicInteger();
|
|
|
|
/**
|
|
* Unique identifier.
|
|
*/
|
|
public final int uniqueId;
|
|
|
|
/**
|
|
* The current ImsCallSessionListenerProxy.
|
|
*/
|
|
private ImsCallSessionListenerProxy mImsCallSessionListenerProxy;
|
|
|
|
/**
|
|
* When calling {@link #terminate(int, int)}, an override for the termination reason which the
|
|
* modem returns.
|
|
*
|
|
* Necessary because passing in an unexpected {@link ImsReasonInfo} reason code to
|
|
* {@link #terminate(int)} will cause the modem to ignore the terminate request.
|
|
*/
|
|
private int mOverrideReason = ImsReasonInfo.CODE_UNSPECIFIED;
|
|
|
|
/**
|
|
* When true, if this call is incoming, it will be answered with an
|
|
* {@link ImsStreamMediaProfile} that has RTT enabled.
|
|
*/
|
|
private boolean mAnswerWithRtt = 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;
|
|
setCallProfile(profile);
|
|
uniqueId = sUniqueIdGenerator.getAndIncrement();
|
|
}
|
|
|
|
/**
|
|
* Closes this object. This object is not usable after being closed.
|
|
*/
|
|
@Override
|
|
public void close() {
|
|
synchronized(mLockObj) {
|
|
if (mSession != null) {
|
|
mSession.close();
|
|
mSession = null;
|
|
} else {
|
|
logi("close :: Cannot close Null call session!");
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replaces the current call profile with a new one, tracking whethere this was previously a
|
|
* video call or not.
|
|
*
|
|
* @param profile The new call profile.
|
|
*/
|
|
@VisibleForTesting
|
|
public void setCallProfile(ImsCallProfile profile) {
|
|
synchronized(mLockObj) {
|
|
mCallProfile = profile;
|
|
trackVideoStateHistory(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 list of conference participants currently
|
|
* associated with this call.
|
|
*
|
|
* @return Copy of the list of conference participants.
|
|
*/
|
|
public List<ConferenceParticipant> getConferenceParticipants() {
|
|
synchronized(mLockObj) {
|
|
logi("getConferenceParticipants :: mConferenceParticipants"
|
|
+ mConferenceParticipants);
|
|
if (mConferenceParticipants == null) {
|
|
return null;
|
|
}
|
|
if (mConferenceParticipants.isEmpty()) {
|
|
return new ArrayList<ConferenceParticipant>(0);
|
|
}
|
|
return new ArrayList<ConferenceParticipant>(mConferenceParticipants);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deliver the bitrate for the indicated media type, direction and bitrate to the upper layer.
|
|
*
|
|
* @param mediaType MediaType is used to identify media stream such as audio or video.
|
|
* @param direction Direction of this packet stream (e.g. uplink or downlink).
|
|
* @param bitsPerSecond This value is the bitrate received from the NW through the Recommended
|
|
* bitrate MAC Control Element message and ImsStack converts this value from MAC bitrate
|
|
* to audio/video codec bitrate (defined in TS26.114).
|
|
* @hide
|
|
*/
|
|
public void callSessionNotifyAnbr(int mediaType, int direction, int bitsPerSecond) {
|
|
synchronized(mLockObj) {
|
|
if (mSession != null) {
|
|
mSession.callSessionNotifyAnbr(mediaType, direction, bitsPerSecond);
|
|
} else {
|
|
logi("callSessionNotifyAnbr : session - null");
|
|
}
|
|
}
|
|
}
|
|
|
|
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 pending a hold operation.
|
|
*
|
|
* @return true if the call is pending a hold operation.
|
|
*/
|
|
public boolean isPendingHold() {
|
|
synchronized(mLockObj) {
|
|
return (mUpdateRequest == UPDATE_HOLD);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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(), mContext.getMainExecutor());
|
|
} 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);
|
|
|
|
synchronized(mLockObj) {
|
|
mSession = session;
|
|
|
|
try {
|
|
session.setListener(createCallSessionListener(), mContext.getMainExecutor());
|
|
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);
|
|
|
|
synchronized(mLockObj) {
|
|
mSession = session;
|
|
mIsConferenceHost = true;
|
|
|
|
try {
|
|
session.setListener(createCallSessionListener(), mContext.getMainExecutor());
|
|
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);
|
|
|
|
if (mAnswerWithRtt) {
|
|
profile.mRttMode = ImsStreamMediaProfile.RTT_MODE_FULL;
|
|
logi("accept :: changing media profile RTT mode to full");
|
|
}
|
|
|
|
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;
|
|
trackVideoStateHistory(mCallProfile);
|
|
mProposedCallProfile = null;
|
|
}
|
|
|
|
// Other call update received
|
|
if (mInCall && (mUpdateRequest == UPDATE_UNSPECIFIED)) {
|
|
mUpdateRequest = UPDATE_NONE;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deflects a call.
|
|
*
|
|
* @param number number to be deflected to.
|
|
* @throws ImsException if the IMS service fails to deflect the call
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
public void deflect(String number) throws ImsException {
|
|
logi("deflect :: session=" + mSession + ", number=" + Rlog.pii(TAG, number));
|
|
|
|
synchronized(mLockObj) {
|
|
if (mSession == null) {
|
|
throw new ImsException("No call to deflect",
|
|
ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
|
|
}
|
|
|
|
try {
|
|
mSession.deflect(number);
|
|
} catch (Throwable t) {
|
|
loge("deflect :: ", t);
|
|
throw new ImsException("deflect()", t, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transfers a call.
|
|
*
|
|
* @param number number to be transferred to.
|
|
* @param isConfirmationRequired indicates blind or assured transfer.
|
|
* @throws ImsException if the IMS service fails to transfer the call.
|
|
*/
|
|
public void transfer(String number, boolean isConfirmationRequired) throws ImsException {
|
|
logi("transfer :: session=" + mSession + ", number=" + Rlog.pii(TAG, number) +
|
|
", isConfirmationRequired=" + isConfirmationRequired);
|
|
|
|
synchronized(mLockObj) {
|
|
if (mSession == null) {
|
|
throw new ImsException("No call to transfer",
|
|
ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
|
|
}
|
|
|
|
try {
|
|
mSession.transfer(number, isConfirmationRequired);
|
|
} catch (Throwable t) {
|
|
loge("transfer :: ", t);
|
|
throw new ImsException("transfer()", t, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transfers a call to another ongoing call.
|
|
*
|
|
* @param transferToImsCall the other ongoing call to which this call will be transferred.
|
|
* @throws ImsException if the IMS service fails to transfer the call.
|
|
*/
|
|
public void consultativeTransfer(ImsCall transferToImsCall) throws ImsException {
|
|
logi("consultativeTransfer :: session=" + mSession + ", other call=" + transferToImsCall);
|
|
|
|
synchronized(mLockObj) {
|
|
if (mSession == null) {
|
|
throw new ImsException("No call to transfer",
|
|
ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED);
|
|
}
|
|
|
|
try {
|
|
mSession.transfer(transferToImsCall.getSession());
|
|
} catch (Throwable t) {
|
|
loge("consultativeTransfer :: ", t);
|
|
throw new ImsException("consultativeTransfer()", t, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void terminate(int reason, int overrideReason) {
|
|
logi("terminate :: reason=" + reason + " ; overrideReason=" + overrideReason);
|
|
mOverrideReason = overrideReason;
|
|
terminate(reason);
|
|
}
|
|
|
|
/**
|
|
* Terminates an IMS call (e.g. user initiated).
|
|
*
|
|
* @param reason reason code to terminate a call
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
public void terminate(int reason) {
|
|
logi("terminate :: reason=" + reason);
|
|
|
|
synchronized(mLockObj) {
|
|
mHold = false;
|
|
mInCall = false;
|
|
mTerminationRequestPending = true;
|
|
|
|
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);
|
|
}
|
|
|
|
// 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;
|
|
mSession.hold(createHoldMediaProfile());
|
|
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());
|
|
}
|
|
}
|
|
|
|
private boolean isUpdatePending(ImsCall imsCall) {
|
|
if (imsCall != null && imsCall.mUpdateRequest != UPDATE_NONE) {
|
|
loge("merge :: update is in progress; request=" +
|
|
updateRequestToString(mUpdateRequest));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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 the fg call of the merge is in the midst of some other operation, we cannot merge.
|
|
// fg is either the host or the peer of the merge
|
|
if (isUpdatePending(this)) {
|
|
setCallSessionMergePending(false);
|
|
if (mMergePeer != null) mMergePeer.setCallSessionMergePending(false);
|
|
if (mMergeHost != null) mMergeHost.setCallSessionMergePending(false);
|
|
throw new ImsException("Call update is in progress",
|
|
ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE);
|
|
}
|
|
|
|
// If the bg call of the merge is in the midst of some other operation, we cannot merge.
|
|
// bg is either the peer or the host of the merge.
|
|
if (isUpdatePending(mMergePeer) || isUpdatePending(mMergeHost)) {
|
|
setCallSessionMergePending(false);
|
|
if (mMergePeer != null) mMergePeer.setCallSessionMergePending(false);
|
|
if (mMergeHost != null) mMergeHost.setCallSessionMergePending(false);
|
|
throw new ImsException("Peer or host 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;
|
|
} else if (mMergeHost != null && !mMergeHost.isMultiparty() && !isMultiparty()) {
|
|
mUpdateRequest = UPDATE_MERGE;
|
|
mMergeHost.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);
|
|
}
|
|
}
|
|
|
|
if (isMultiparty()) {
|
|
mMergeRequestedByConference = true;
|
|
} else {
|
|
logi("merge : mMergeRequestedByConference not set");
|
|
}
|
|
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 :: session=" + mSession);
|
|
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 <a href="http://tools.ietf.org/html/rfc2833">RFC 2833</a>,
|
|
* 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 :: ");
|
|
|
|
synchronized(mLockObj) {
|
|
if (mSession != null) {
|
|
mSession.sendDtmf(c, result);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2833</a>,
|
|
* 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 :: ");
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
public void sendRttMessage(String rttMessage) {
|
|
synchronized(mLockObj) {
|
|
if (mSession == null) {
|
|
loge("sendRttMessage::no session, ignoring");
|
|
return;
|
|
}
|
|
if (mCallProfile == null || mCallProfile.mMediaProfile == null) {
|
|
loge("sendRttMessage:: no valid call profile, ignoring");
|
|
return;
|
|
}
|
|
if (!mCallProfile.mMediaProfile.isRttCall()) {
|
|
logi("sendRttMessage::Not an rtt call, ignoring");
|
|
return;
|
|
}
|
|
mSession.sendRttMessage(rttMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends a user-requested RTT upgrade request.
|
|
* @param rttOn true if the request is to turn on RTT, false to turn off.
|
|
*/
|
|
public void sendRttModifyRequest(boolean rttOn) {
|
|
logi("sendRttModifyRequest");
|
|
|
|
synchronized(mLockObj) {
|
|
if (mSession == null) {
|
|
loge("sendRttModifyRequest::no session, ignoring");
|
|
return;
|
|
}
|
|
if (mCallProfile == null || mCallProfile.mMediaProfile == null) {
|
|
loge("sendRttModifyRequest:: no valid call profile, ignoring");
|
|
return;
|
|
}
|
|
if (rttOn && mCallProfile.mMediaProfile.isRttCall()) {
|
|
logi("sendRttModifyRequest::Already RTT call, ignoring request to turn on.");
|
|
return;
|
|
} else if (!rttOn && !mCallProfile.mMediaProfile.isRttCall()) {
|
|
logi("sendRttModifyRequest::Not RTT call, ignoring request to turn off.");
|
|
return;
|
|
}
|
|
// Make a copy of the current ImsCallProfile and modify it to enable RTT
|
|
Parcel p = Parcel.obtain();
|
|
mCallProfile.writeToParcel(p, 0);
|
|
p.setDataPosition(0);
|
|
ImsCallProfile requestedProfile = new ImsCallProfile(p);
|
|
requestedProfile.mMediaProfile.setRttMode(rttOn
|
|
? ImsStreamMediaProfile.RTT_MODE_FULL
|
|
: ImsStreamMediaProfile.RTT_MODE_DISABLED);
|
|
|
|
mSession.sendRttModifyRequest(requestedProfile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends the user's response to a remotely-issued RTT upgrade request
|
|
*
|
|
* @param textStream A valid {@link Connection.RttTextStream} if the user
|
|
* accepts, {@code null} if not.
|
|
*/
|
|
public void sendRttModifyResponse(boolean status) {
|
|
logi("sendRttModifyResponse");
|
|
|
|
synchronized(mLockObj) {
|
|
if (mSession == null) {
|
|
loge("sendRttModifyResponse::no session");
|
|
return;
|
|
}
|
|
if (mCallProfile == null || mCallProfile.mMediaProfile == null) {
|
|
loge("sendRttModifyResponse:: no valid call profile, ignoring");
|
|
return;
|
|
}
|
|
if (mCallProfile.mMediaProfile.isRttCall()) {
|
|
logi("sendRttModifyResponse::Already RTT call, ignoring.");
|
|
return;
|
|
}
|
|
mSession.sendRttModifyResponse(status);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Requests that RTP header extensions are added to the next RTP packet sent by the IMS stack.
|
|
* <p>
|
|
* The {@link RtpHeaderExtension#getLocalIdentifier()} local identifiers specified here must match
|
|
* agreed upon identifiers as indicated in
|
|
* {@link ImsCallProfile#getAcceptedRtpHeaderExtensionTypes()} for the current
|
|
* {@link #getCallProfile()}.
|
|
* <p>
|
|
* By specification, the RTP header extension is an unacknowledged transmission and there is no
|
|
* guarantee that the header extension will be delivered by the network to the other end of the
|
|
* call.
|
|
* @param rtpHeaderExtensions The RTP header extension(s) to be included in the next RTP
|
|
* packet.
|
|
*/
|
|
public void sendRtpHeaderExtensions(@NonNull Set<RtpHeaderExtension> rtpHeaderExtensions) {
|
|
logi("sendRtpHeaderExtensions; extensionsSent=" + rtpHeaderExtensions.size());
|
|
synchronized(mLockObj) {
|
|
if (mSession == null) {
|
|
loge("sendRtpHeaderExtensions::no session");
|
|
}
|
|
mSession.sendRtpHeaderExtensions(rtpHeaderExtensions);
|
|
}
|
|
}
|
|
|
|
public void setAnswerWithRtt() {
|
|
mAnswerWithRtt = true;
|
|
}
|
|
|
|
private void clear(ImsReasonInfo lastReasonInfo) {
|
|
mInCall = false;
|
|
mHold = false;
|
|
mUpdateRequest = UPDATE_NONE;
|
|
mLastReasonInfo = lastReasonInfo;
|
|
}
|
|
|
|
/**
|
|
* Creates an IMS call session listener.
|
|
*/
|
|
private ImsCallSession.Listener createCallSessionListener() {
|
|
mImsCallSessionListenerProxy = new ImsCallSessionListenerProxy();
|
|
return mImsCallSessionListenerProxy;
|
|
}
|
|
|
|
/**
|
|
* @return the current ImsCallSessionListenerProxy. NOTE: ONLY FOR USE WITH TESTING.
|
|
*/
|
|
@VisibleForTesting
|
|
public ImsCallSessionListenerProxy getImsCallSessionListenerProxy() {
|
|
return mImsCallSessionListenerProxy;
|
|
}
|
|
|
|
/**
|
|
* @return the current Listener. NOTE: ONLY FOR USE WITH TESTING.
|
|
*/
|
|
@VisibleForTesting
|
|
public Listener getListener() {
|
|
return mListener;
|
|
}
|
|
|
|
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) {
|
|
if (state == null || state.mParticipants == null) {
|
|
return;
|
|
}
|
|
|
|
mConferenceParticipants = parseConferenceState(state);
|
|
|
|
if (mConferenceParticipants != null && mListener != null) {
|
|
try {
|
|
mListener.onConferenceParticipantsStateChanged(this, mConferenceParticipants);
|
|
} catch (Throwable t) {
|
|
loge("notifyConferenceStateUpdated :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static List<ConferenceParticipant> parseConferenceState(ImsConferenceState state) {
|
|
Set<Entry<String, Bundle>> participants = state.mParticipants.entrySet();
|
|
|
|
if (participants == null) {
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
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 (CONF_DBG) {
|
|
Log.i(TAG, "notifyConferenceStateUpdated :: key=" + Rlog.pii(TAG, key) +
|
|
", status=" + status +
|
|
", user=" + Rlog.pii(TAG, user) +
|
|
", displayName= " + Rlog.pii(TAG, displayName) +
|
|
", endpoint=" + Rlog.pii(TAG, endpoint));
|
|
}
|
|
|
|
Uri handle = Uri.parse(user);
|
|
if (endpoint == null) {
|
|
endpoint = "";
|
|
}
|
|
Uri endpointUri = Uri.parse(endpoint);
|
|
int connectionState = ImsConferenceState.getConnectionStateForStatus(status);
|
|
|
|
if (connectionState != Connection.STATE_DISCONNECTED) {
|
|
ConferenceParticipant conferenceParticipant = new ConferenceParticipant(handle,
|
|
displayName, endpointUri, connectionState, Call.Details.DIRECTION_UNKNOWN);
|
|
conferenceParticipants.add(conferenceParticipant);
|
|
}
|
|
}
|
|
return conferenceParticipants;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
private void processCallTerminated(ImsReasonInfo reasonInfo) {
|
|
logi("processCallTerminated :: reason=" + reasonInfo + " userInitiated = " +
|
|
mTerminationRequestPending);
|
|
|
|
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.
|
|
// We will also NOT bury the termination if a termination was initiated locally.
|
|
if (isCallSessionMergePending() && !mTerminationRequestPending) {
|
|
// 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 (CONF_DBG) {
|
|
logi("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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
private boolean isTransientConferenceSession(ImsCallSession session) {
|
|
if (session != null && session != mSession && session == mTransientConferenceSession) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void setTransientSessionAsPrimary(ImsCallSession transientSession) {
|
|
synchronized (ImsCall.this) {
|
|
mSession.setListener(null, null);
|
|
mSession = transientSession;
|
|
mSession.setListener(createCallSessionListener(), mContext.getMainExecutor());
|
|
}
|
|
}
|
|
|
|
private void markCallAsMerged(boolean playDisconnectTone) {
|
|
if (!isSessionAlive(mSession)) {
|
|
// If the peer is dead, let's not play a disconnect sound for it when we
|
|
// unbury the termination callback.
|
|
logi("markCallAsMerged");
|
|
setIsMerged(playDisconnectTone);
|
|
mSessionEndDuringMerge = true;
|
|
String reasonInfo;
|
|
int reasonCode = ImsReasonInfo.CODE_UNSPECIFIED;
|
|
if (playDisconnectTone) {
|
|
reasonCode = ImsReasonInfo.CODE_USER_TERMINATED_BY_REMOTE;
|
|
reasonInfo = "Call ended by network";
|
|
} else {
|
|
reasonCode = ImsReasonInfo.CODE_LOCAL_ENDED_BY_CONFERENCE_MERGE;
|
|
reasonInfo = "Call ended during conference merge process.";
|
|
}
|
|
mSessionEndDuringMergeReasonInfo = new ImsReasonInfo(
|
|
reasonCode, 0, reasonInfo);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the merge was requested by foreground conference call
|
|
*
|
|
* @return true if the merge was requested by foreground conference call
|
|
*/
|
|
public boolean isMergeRequestedByConf() {
|
|
synchronized(mLockObj) {
|
|
return mMergeRequestedByConference;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets the flag which indicates merge request was sent by
|
|
* foreground conference call
|
|
*/
|
|
public void resetIsMergeRequestedByConf(boolean value) {
|
|
synchronized(mLockObj) {
|
|
mMergeRequestedByConference = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns current ImsCallSession
|
|
*
|
|
* @return current session
|
|
*/
|
|
public ImsCallSession getSession() {
|
|
synchronized(mLockObj) {
|
|
return mSession;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
*/
|
|
private void processMergeComplete() {
|
|
logi("processMergeComplete :: ");
|
|
|
|
// 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;
|
|
|
|
ImsCall finalHostCall;
|
|
ImsCall finalPeerCall;
|
|
|
|
synchronized(ImsCall.this) {
|
|
if (isMultiparty()) {
|
|
setIsMerged(false);
|
|
// if case handles Case 4 explained in callSessionMergeComplete
|
|
// otherwise it is case 5
|
|
if (!mMergeRequestedByConference) {
|
|
// single call in fg, conference call in bg.
|
|
// Finally conf call becomes active after conference
|
|
this.mHold = false;
|
|
swapRequired = true;
|
|
}
|
|
mMergePeer.markCallAsMerged(false);
|
|
finalHostCall = this;
|
|
finalPeerCall = mMergePeer;
|
|
} 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, 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.
|
|
|
|
// handles Case 3 explained in callSessionMergeComplete
|
|
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.
|
|
mMergePeer.mHold = false;
|
|
this.mHold = true;
|
|
if (mConferenceParticipants != null && !mConferenceParticipants.isEmpty()) {
|
|
mMergePeer.mConferenceParticipants = mConferenceParticipants;
|
|
}
|
|
// At this point both host & peer will have participant information.
|
|
// Peer will transition to host & the participant information
|
|
// from that will be used
|
|
// HostCall that failed to merge will remain as a single call with
|
|
// mConferenceParticipants, which should not be used.
|
|
// Expectation is that if this call becomes part of a conference call in future,
|
|
// mConferenceParticipants will be overriten with new CEP that is received.
|
|
finalHostCall = mMergePeer;
|
|
finalPeerCall = this;
|
|
swapRequired = true;
|
|
setIsMerged(false);
|
|
mMergePeer.setIsMerged(false);
|
|
if (CONF_DBG) {
|
|
logi("processMergeComplete :: transient will transfer to merge peer");
|
|
}
|
|
} else if (!isSessionAlive(mSession) &&
|
|
isSessionAlive(mMergePeer.getCallSession())) {
|
|
// Handles case 2 explained in callSessionMergeComplete
|
|
// 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 (CONF_DBG) {
|
|
logi("processMergeComplete :: transient will stay with the merge host");
|
|
}
|
|
} else {
|
|
// Handles case 1 explained in callSessionMergeComplete
|
|
// 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;
|
|
mMergePeer.markCallAsMerged(false);
|
|
swapRequired = false;
|
|
setIsMerged(false);
|
|
mMergePeer.setIsMerged(true);
|
|
if (CONF_DBG) {
|
|
logi("processMergeComplete :: transient will stay with us (I'm the host).");
|
|
}
|
|
}
|
|
|
|
if (CONF_DBG) {
|
|
logi("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;
|
|
|
|
updateCallProfile(finalPeerCall);
|
|
updateCallProfile(finalHostCall);
|
|
|
|
// 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();
|
|
|
|
// Keep track of the fact that merge host is the origin of a conference call in
|
|
// progress. This is important so that we can later determine if a multiparty ImsCall
|
|
// is multiparty because it was the origin of a conference call, or because it is a
|
|
// member of a conference on another device.
|
|
finalHostCall.mIsConferenceHost = true;
|
|
}
|
|
if (listener != null) {
|
|
try {
|
|
// finalPeerCall will have the participant that was not merged and
|
|
// it will be held state
|
|
// if peer was merged successfully, finalPeerCall will be null
|
|
listener.onCallMerged(finalHostCall, finalPeerCall, swapRequired);
|
|
} catch (Throwable t) {
|
|
loge("processMergeComplete :: ", t);
|
|
}
|
|
if (mConferenceParticipants != null && !mConferenceParticipants.isEmpty()) {
|
|
try {
|
|
listener.onConferenceParticipantsStateChanged(finalHostCall,
|
|
mConferenceParticipants);
|
|
} catch (Throwable t) {
|
|
loge("processMergeComplete :: ", t);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
private static void updateCallProfile(ImsCall call) {
|
|
if (call != null) {
|
|
call.updateCallProfile();
|
|
}
|
|
}
|
|
|
|
private void updateCallProfile() {
|
|
synchronized (mLockObj) {
|
|
if (mSession != null) {
|
|
setCallProfile(mSession.getCallProfile());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the case where the session has ended during a merge by reporting the termination
|
|
* reason to listeners.
|
|
*/
|
|
private void notifySessionTerminatedDuringMerge() {
|
|
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 (CONF_DBG) {
|
|
logi("notifySessionTerminatedDuringMerge ::reporting terminate during merge");
|
|
}
|
|
notifyFailure = true;
|
|
notifyFailureReasonInfo = mSessionEndDuringMergeReasonInfo;
|
|
}
|
|
clearSessionTerminationFlags();
|
|
}
|
|
|
|
if (listener != null && notifyFailure) {
|
|
try {
|
|
processCallTerminated(notifyFailureReasonInfo);
|
|
} catch (Throwable t) {
|
|
loge("notifySessionTerminatedDuringMerge :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void clearSessionTerminationFlags() {
|
|
mSessionEndDuringMerge = false;
|
|
mSessionEndDuringMergeReasonInfo = null;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
private void processMergeFailed(ImsReasonInfo reasonInfo) {
|
|
logi("processMergeFailed :: 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;
|
|
}
|
|
|
|
// Try to clean up the transient session if it exists.
|
|
if (mTransientConferenceSession != null) {
|
|
mTransientConferenceSession.setListener(null, null);
|
|
mTransientConferenceSession = null;
|
|
}
|
|
|
|
listener = mListener;
|
|
|
|
// Ensure the calls being conferenced into the conference has isMerged = false.
|
|
// Ensure any terminations are surfaced from this session.
|
|
markCallAsMerged(true);
|
|
setCallSessionMergePending(false);
|
|
notifySessionTerminatedDuringMerge();
|
|
|
|
// Perform the same cleanup on the merge peer if it exists.
|
|
if (mMergePeer != null) {
|
|
mMergePeer.markCallAsMerged(true);
|
|
mMergePeer.setCallSessionMergePending(false);
|
|
mMergePeer.notifySessionTerminatedDuringMerge();
|
|
} else {
|
|
loge("processMergeFailed :: No merge peer!");
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
public class ImsCallSessionListenerProxy extends ImsCallSession.Listener {
|
|
@Override
|
|
public void callSessionInitiating(ImsCallSession session, ImsCallProfile profile) {
|
|
logi("callSessionInitiating :: session=" + session + " profile=" + profile);
|
|
if (isTransientConferenceSession(session)) {
|
|
// If it is a transient (conference) session, there is no action for this signal.
|
|
logi("callSessionInitiating :: not supported for transient conference session=" +
|
|
session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
setCallProfile(profile);
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallInitiating(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionInitiating :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionProgressing(ImsCallSession session, ImsStreamMediaProfile profile) {
|
|
logi("callSessionProgressing :: session=" + session + " profile=" + profile);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
// If it is a transient (conference) session, there is no action for this signal.
|
|
logi("callSessionProgressing :: not supported for transient conference session=" +
|
|
session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
ImsCallProfile updatedProfile = session.getCallProfile();
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
// The ImsCallProfile may have updated here (for example call state change). Query
|
|
// the potentially updated call profile to pick up these changes.
|
|
setCallProfile(updatedProfile);
|
|
// Apply the new mediaProfile on top of the Call Profile so it is not ignored in
|
|
// case the ImsService has not had a chance to update it yet.
|
|
if( mCallProfile != null && mCallProfile.mMediaProfile != null) {
|
|
mCallProfile.mMediaProfile.copyFrom(profile);
|
|
} else {
|
|
logi("Call was closed already, skip updating CallProfile");
|
|
}
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallProgressing(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionProgressing :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionStarted(ImsCallSession session, ImsCallProfile profile) {
|
|
logi("callSessionStarted :: session=" + session + " profile=" + profile);
|
|
|
|
if (!isTransientConferenceSession(session)) {
|
|
// In the case that we are in the middle of a merge (either host or peer), we have
|
|
// closure as far as this call's primary session is concerned. If we are not
|
|
// merging...its a NOOP.
|
|
setCallSessionMergePending(false);
|
|
} else {
|
|
logi("callSessionStarted :: on transient session=" + session);
|
|
return;
|
|
}
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
// No further processing is needed if this is the transient session.
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
setCallProfile(profile);
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallStarted(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionStarted :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionStartFailed(ImsCallSession session, ImsReasonInfo reasonInfo) {
|
|
loge("callSessionStartFailed :: session=" + session + " reasonInfo=" + reasonInfo);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
// We should not get this callback for a transient session.
|
|
logi("callSessionStartFailed :: not supported for transient conference session=" +
|
|
session);
|
|
return;
|
|
}
|
|
|
|
if (mIsConferenceHost) {
|
|
// If the dial request was a adhoc conf calling one, this call would have
|
|
// been marked the conference host as part of the request.
|
|
mIsConferenceHost = false;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
mLastReasonInfo = reasonInfo;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallStartFailed(ImsCall.this, reasonInfo);
|
|
} catch (Throwable t) {
|
|
loge("callSessionStarted :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionTerminated(ImsCallSession session, ImsReasonInfo reasonInfo) {
|
|
logi("callSessionTerminated :: session=" + session + " reasonInfo=" + reasonInfo);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionTerminated :: on transient session=" + session);
|
|
// This is bad, it should be treated much a callSessionMergeFailed since the
|
|
// transient session only exists when in the process of a merge and the
|
|
// termination of this session is effectively the end of the merge.
|
|
processMergeFailed(reasonInfo);
|
|
return;
|
|
}
|
|
|
|
if (mOverrideReason != ImsReasonInfo.CODE_UNSPECIFIED) {
|
|
logi("callSessionTerminated :: overrideReasonInfo=" + mOverrideReason);
|
|
reasonInfo = new ImsReasonInfo(mOverrideReason, reasonInfo.getExtraCode(),
|
|
reasonInfo.getExtraMessage());
|
|
}
|
|
|
|
// Process the termination first. If we are in the midst of establishing a conference
|
|
// call, we may bury this callback until we are done. If there so no conference
|
|
// call, the code after this function will be a NOOP.
|
|
processCallTerminated(reasonInfo);
|
|
|
|
// If session has terminated, it is no longer pending merge.
|
|
setCallSessionMergePending(false);
|
|
|
|
}
|
|
|
|
@Override
|
|
public void callSessionHeld(ImsCallSession session, ImsCallProfile profile) {
|
|
logi("callSessionHeld :: session=" + session + "profile=" + profile);
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
// If the session was held, it is no longer pending a merge -- this means it could
|
|
// not be merged into the conference and was held instead.
|
|
setCallSessionMergePending(false);
|
|
|
|
setCallProfile(profile);
|
|
|
|
if (mUpdateRequest == UPDATE_HOLD_MERGE) {
|
|
// This hold request was made to set the stage for a merge.
|
|
mergeInternal();
|
|
return;
|
|
}
|
|
|
|
mHold = true;
|
|
mUpdateRequest = UPDATE_NONE;
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallHeld(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionHeld :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionHoldFailed(ImsCallSession session, ImsReasonInfo reasonInfo) {
|
|
loge("callSessionHoldFailed :: session" + session + "reasonInfo=" + reasonInfo);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
// We should not get this callback for a transient session.
|
|
logi("callSessionHoldFailed :: not supported for transient conference session=" +
|
|
session);
|
|
return;
|
|
}
|
|
|
|
logi("callSessionHoldFailed :: session=" + session +
|
|
", reasonInfo=" + reasonInfo);
|
|
|
|
synchronized (mLockObj) {
|
|
mHold = false;
|
|
}
|
|
|
|
boolean isHoldForMerge = false;
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
if (mUpdateRequest == UPDATE_HOLD_MERGE) {
|
|
isHoldForMerge = true;
|
|
}
|
|
|
|
mUpdateRequest = UPDATE_NONE;
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallHoldFailed(ImsCall.this, reasonInfo);
|
|
} catch (Throwable t) {
|
|
loge("callSessionHoldFailed :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates that an {@link ImsCallSession} has been remotely held. This can be due to the
|
|
* remote party holding the current call, or swapping between calls.
|
|
* @param session the session which was held.
|
|
* @param profile the profile for the held call.
|
|
*/
|
|
@Override
|
|
public void callSessionHoldReceived(ImsCallSession session, ImsCallProfile profile) {
|
|
logi("callSessionHoldReceived :: session=" + session + "profile=" + profile);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
// We should not get this callback for a transient session.
|
|
logi("callSessionHoldReceived :: not supported for transient conference session=" +
|
|
session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
setCallProfile(profile);
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallHoldReceived(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionHoldReceived :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates that an {@link ImsCallSession} has been remotely resumed. This can be due to
|
|
* the remote party un-holding the current call, or swapping back to this call.
|
|
* @param session the session which was resumed.
|
|
* @param profile the profile for the held call.
|
|
*/
|
|
@Override
|
|
public void callSessionResumed(ImsCallSession session, ImsCallProfile profile) {
|
|
logi("callSessionResumed :: session=" + session + "profile=" + profile);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionResumed :: not supported for transient conference session=" +
|
|
session);
|
|
return;
|
|
}
|
|
|
|
// If this call was pending a merge, it is not anymore. This is the case when we
|
|
// are merging in a new call into an existing conference.
|
|
setCallSessionMergePending(false);
|
|
|
|
// TOOD: When we are merging a new call into an existing conference we are waiting
|
|
// for 2 triggers to let us know that the conference has been established, the first
|
|
// is a termination for the new calls (since it is added to the conference) the second
|
|
// would be a resume on the existing conference. If the resume comes first, then
|
|
// we will make the onCallResumed() callback and its unclear how this will behave if
|
|
// the termination has not come yet.
|
|
|
|
ImsCall.Listener listener;
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
setCallProfile(profile);
|
|
mUpdateRequest = UPDATE_NONE;
|
|
mHold = false;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallResumed(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionResumed :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionResumeFailed(ImsCallSession session, ImsReasonInfo reasonInfo) {
|
|
loge("callSessionResumeFailed :: session=" + session + "reasonInfo=" + reasonInfo);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionResumeFailed :: not supported for transient conference session=" +
|
|
session);
|
|
return;
|
|
}
|
|
|
|
synchronized(mLockObj) {
|
|
mHold = true;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
mUpdateRequest = UPDATE_NONE;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallResumeFailed(ImsCall.this, reasonInfo);
|
|
} catch (Throwable t) {
|
|
loge("callSessionResumeFailed :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionResumeReceived(ImsCallSession session, ImsCallProfile profile) {
|
|
logi("callSessionResumeReceived :: session=" + session + "profile=" + profile);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionResumeReceived :: not supported for transient conference session=" +
|
|
session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
setCallProfile(profile);
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallResumeReceived(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionResumeReceived :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionMergeStarted(ImsCallSession session,
|
|
ImsCallSession newSession, ImsCallProfile profile) {
|
|
logi("callSessionMergeStarted :: session=" + session + " newSession=" + newSession +
|
|
", profile=" + profile);
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* We received a callback from ImsCallSession that merge completed.
|
|
* @param newSession - this session can have 2 values based on the below scenarios
|
|
*
|
|
* Conference Scenarios :
|
|
* Case 1 - 3 way success case
|
|
* Case 2 - 3 way success case but held call fails to merge
|
|
* Case 3 - 3 way success case but active call fails to merge
|
|
* case 4 - 4 way success case, where merge is initiated on the foreground single-party
|
|
* call and the conference (mergeHost) is the background call.
|
|
* case 5 - 4 way success case, where merge is initiated on the foreground conference
|
|
* call (mergeHost) and the single party call is in the background.
|
|
*
|
|
* Conference Result:
|
|
* session : new session after conference
|
|
* newSession = new session for case 1, 2, 3.
|
|
* Should be considered as mTransientConferencession
|
|
* newSession = Active conference session for case 5 will be null
|
|
* mergehost was foreground call
|
|
* mTransientConferencession will be null
|
|
* newSession = Active conference session for case 4 will be null
|
|
* mergeHost was background call
|
|
* mTransientConferencession will be null
|
|
*/
|
|
@Override
|
|
public void callSessionMergeComplete(ImsCallSession newSession) {
|
|
logi("callSessionMergeComplete :: newSession =" + newSession);
|
|
if (!isMergeHost()) {
|
|
// Handles case 4
|
|
mMergeHost.processMergeComplete();
|
|
} else {
|
|
// Handles case 1, 2, 3
|
|
if (newSession != null) {
|
|
mTransientConferenceSession = newSession;
|
|
}
|
|
// Handles case 5
|
|
processMergeComplete();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionMergeFailed(ImsCallSession session, ImsReasonInfo reasonInfo) {
|
|
loge("callSessionMergeFailed :: session=" + session + "reasonInfo=" + reasonInfo);
|
|
|
|
// Its possible that there could be threading issues with the other thread handling
|
|
// the other call. This could affect our state.
|
|
synchronized (ImsCall.this) {
|
|
// Let's tell our parent ImsCall that the merge has failed and we need to clean
|
|
// up any temporary, transient state. Note this only gets called for an initial
|
|
// conference. If a merge into an existing conference fails, the two sessions will
|
|
// just go back to their original state (ACTIVE or HELD).
|
|
if (isMergeHost()) {
|
|
processMergeFailed(reasonInfo);
|
|
} else if (mMergeHost != null) {
|
|
mMergeHost.processMergeFailed(reasonInfo);
|
|
} else {
|
|
loge("callSessionMergeFailed :: No merge host for this conference!");
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionUpdated(ImsCallSession session, ImsCallProfile profile) {
|
|
logi("callSessionUpdated :: session=" + session + " profile=" + profile);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionUpdated :: not supported for transient conference session=" +
|
|
session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
setCallProfile(profile);
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallUpdated(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionUpdated :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionUpdateFailed(ImsCallSession session, ImsReasonInfo reasonInfo) {
|
|
loge("callSessionUpdateFailed :: session=" + session + " reasonInfo=" + reasonInfo);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionUpdateFailed :: not supported for transient conference session=" +
|
|
session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
mUpdateRequest = UPDATE_NONE;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallUpdateFailed(ImsCall.this, reasonInfo);
|
|
} catch (Throwable t) {
|
|
loge("callSessionUpdateFailed :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionUpdateReceived(ImsCallSession session, ImsCallProfile profile) {
|
|
logi("callSessionUpdateReceived :: session=" + session + " profile=" + profile);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionUpdateReceived :: not supported for transient conference " +
|
|
"session=" + session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
mProposedCallProfile = profile;
|
|
mUpdateRequest = UPDATE_UNSPECIFIED;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallUpdateReceived(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionUpdateReceived :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionConferenceExtended(ImsCallSession session, ImsCallSession newSession,
|
|
ImsCallProfile profile) {
|
|
logi("callSessionConferenceExtended :: session=" + session + " newSession=" +
|
|
newSession + ", profile=" + profile);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionConferenceExtended :: not supported for transient conference " +
|
|
"session=" + session);
|
|
return;
|
|
}
|
|
|
|
ImsCall newCall = createNewCall(newSession, profile);
|
|
|
|
if (newCall == null) {
|
|
callSessionConferenceExtendFailed(session, new ImsReasonInfo());
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
mUpdateRequest = UPDATE_NONE;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallConferenceExtended(ImsCall.this, newCall);
|
|
} catch (Throwable t) {
|
|
loge("callSessionConferenceExtended :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionConferenceExtendFailed(ImsCallSession session,
|
|
ImsReasonInfo reasonInfo) {
|
|
loge("callSessionConferenceExtendFailed :: reasonInfo=" + reasonInfo);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionConferenceExtendFailed :: not supported for transient " +
|
|
"conference session=" + session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
mUpdateRequest = UPDATE_NONE;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallConferenceExtendFailed(ImsCall.this, reasonInfo);
|
|
} catch (Throwable t) {
|
|
loge("callSessionConferenceExtendFailed :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionConferenceExtendReceived(ImsCallSession session,
|
|
ImsCallSession newSession, ImsCallProfile profile) {
|
|
logi("callSessionConferenceExtendReceived :: newSession=" + newSession +
|
|
", profile=" + profile);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionConferenceExtendReceived :: not supported for transient " +
|
|
"conference session" + session);
|
|
return;
|
|
}
|
|
|
|
ImsCall newCall = createNewCall(newSession, profile);
|
|
|
|
if (newCall == null) {
|
|
// Should all the calls be terminated...???
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallConferenceExtendReceived(ImsCall.this, newCall);
|
|
} catch (Throwable t) {
|
|
loge("callSessionConferenceExtendReceived :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionInviteParticipantsRequestDelivered(ImsCallSession session) {
|
|
logi("callSessionInviteParticipantsRequestDelivered ::");
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionInviteParticipantsRequestDelivered :: not supported for " +
|
|
"conference session=" + session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
mIsConferenceHost = true;
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallInviteParticipantsRequestDelivered(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionInviteParticipantsRequestDelivered :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionInviteParticipantsRequestFailed(ImsCallSession session,
|
|
ImsReasonInfo reasonInfo) {
|
|
loge("callSessionInviteParticipantsRequestFailed :: reasonInfo=" + reasonInfo);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionInviteParticipantsRequestFailed :: not supported for " +
|
|
"conference session=" + session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallInviteParticipantsRequestFailed(ImsCall.this, reasonInfo);
|
|
} catch (Throwable t) {
|
|
loge("callSessionInviteParticipantsRequestFailed :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionRemoveParticipantsRequestDelivered(ImsCallSession session) {
|
|
logi("callSessionRemoveParticipantsRequestDelivered ::");
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionRemoveParticipantsRequestDelivered :: not supported for " +
|
|
"conference session=" + session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallRemoveParticipantsRequestDelivered(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionRemoveParticipantsRequestDelivered :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionRemoveParticipantsRequestFailed(ImsCallSession session,
|
|
ImsReasonInfo reasonInfo) {
|
|
loge("callSessionRemoveParticipantsRequestFailed :: reasonInfo=" + reasonInfo);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionRemoveParticipantsRequestFailed :: not supported for " +
|
|
"conference session=" + session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallRemoveParticipantsRequestFailed(ImsCall.this, reasonInfo);
|
|
} catch (Throwable t) {
|
|
loge("callSessionRemoveParticipantsRequestFailed :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionConferenceStateUpdated(ImsCallSession session,
|
|
ImsConferenceState state) {
|
|
logi("callSessionConferenceStateUpdated :: state=" + state);
|
|
conferenceStateUpdated(state);
|
|
}
|
|
|
|
@Override
|
|
public void callSessionUssdMessageReceived(ImsCallSession session, int mode,
|
|
String ussdMessage) {
|
|
logi("callSessionUssdMessageReceived :: mode=" + mode + ", ussdMessage=" +
|
|
ussdMessage);
|
|
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionUssdMessageReceived :: not supported for transient " +
|
|
"conference session=" + session);
|
|
return;
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallUssdMessageReceived(ImsCall.this, mode, ussdMessage);
|
|
} catch (Throwable t) {
|
|
loge("callSessionUssdMessageReceived :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionTtyModeReceived(ImsCallSession session, int mode) {
|
|
logi("callSessionTtyModeReceived :: mode=" + mode);
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallSessionTtyModeReceived(ImsCall.this, mode);
|
|
} catch (Throwable t) {
|
|
loge("callSessionTtyModeReceived :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notifies of a change to the multiparty state for this {@code ImsCallSession}.
|
|
*
|
|
* @param session The call session.
|
|
* @param isMultiParty {@code true} if the session became multiparty, {@code false}
|
|
* otherwise.
|
|
*/
|
|
@Override
|
|
public void callSessionMultipartyStateChanged(ImsCallSession session,
|
|
boolean isMultiParty) {
|
|
if (VDBG) {
|
|
logi("callSessionMultipartyStateChanged isMultiParty: " + (isMultiParty ? "Y"
|
|
: "N"));
|
|
}
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onMultipartyStateChanged(ImsCall.this, isMultiParty);
|
|
} catch (Throwable t) {
|
|
loge("callSessionMultipartyStateChanged :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void callSessionHandover(ImsCallSession session, int srcNetworkType,
|
|
int targetNetworkType, ImsReasonInfo reasonInfo) {
|
|
logi("callSessionHandover :: session=" + session + ", srcAccessTech=" +
|
|
srcNetworkType + ", targetAccessTech=" + targetNetworkType + ", reasonInfo=" +
|
|
reasonInfo);
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallHandover(ImsCall.this,
|
|
ServiceState.networkTypeToRilRadioTechnology(srcNetworkType),
|
|
ServiceState.networkTypeToRilRadioTechnology(targetNetworkType),
|
|
reasonInfo);
|
|
} catch (Throwable t) {
|
|
loge("callSessionHandover :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionHandoverFailed(ImsCallSession session, int srcNetworkType,
|
|
int targetNetworkType, ImsReasonInfo reasonInfo) {
|
|
loge("callSessionHandoverFailed :: session=" + session + ", srcAccessTech=" +
|
|
srcNetworkType + ", targetAccessTech=" + targetNetworkType + ", reasonInfo=" +
|
|
reasonInfo);
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallHandoverFailed(ImsCall.this,
|
|
ServiceState.networkTypeToRilRadioTechnology(srcNetworkType),
|
|
ServiceState.networkTypeToRilRadioTechnology(targetNetworkType),
|
|
reasonInfo);
|
|
} catch (Throwable t) {
|
|
loge("callSessionHandoverFailed :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionSuppServiceReceived(ImsCallSession session,
|
|
ImsSuppServiceNotification suppServiceInfo ) {
|
|
if (isTransientConferenceSession(session)) {
|
|
logi("callSessionSuppServiceReceived :: not supported for transient conference"
|
|
+ " session=" + session);
|
|
return;
|
|
}
|
|
|
|
logi("callSessionSuppServiceReceived :: session=" + session +
|
|
", suppServiceInfo" + suppServiceInfo);
|
|
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallSuppServiceReceived(ImsCall.this, suppServiceInfo);
|
|
} catch (Throwable t) {
|
|
loge("callSessionSuppServiceReceived :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionRttModifyRequestReceived(ImsCallSession session,
|
|
ImsCallProfile callProfile) {
|
|
ImsCall.Listener listener;
|
|
logi("callSessionRttModifyRequestReceived");
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (!callProfile.mMediaProfile.isRttCall()) {
|
|
logi("callSessionRttModifyRequestReceived:: ignoring request, requested profile " +
|
|
"is not RTT.");
|
|
return;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onRttModifyRequestReceived(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionRttModifyRequestReceived:: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionRttModifyResponseReceived(int status) {
|
|
ImsCall.Listener listener;
|
|
|
|
logi("callSessionRttModifyResponseReceived: " + status);
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onRttModifyResponseReceived(ImsCall.this, status);
|
|
} catch (Throwable t) {
|
|
loge("callSessionRttModifyResponseReceived:: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionRttMessageReceived(String rttMessage) {
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onRttMessageReceived(ImsCall.this, rttMessage);
|
|
} catch (Throwable t) {
|
|
loge("callSessionRttMessageReceived:: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionRttAudioIndicatorChanged(ImsStreamMediaProfile profile) {
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onRttAudioIndicatorChanged(ImsCall.this, profile);
|
|
} catch (Throwable t) {
|
|
loge("callSessionRttAudioIndicatorChanged:: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionTransferred(ImsCallSession session) {
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallSessionTransferred(ImsCall.this);
|
|
} catch (Throwable t) {
|
|
loge("callSessionTransferred:: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionTransferFailed(ImsCallSession session, ImsReasonInfo reasonInfo) {
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallSessionTransferFailed(ImsCall.this, reasonInfo);
|
|
} catch (Throwable t) {
|
|
loge("callSessionTransferFailed:: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionDtmfReceived(char digit) {
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized(ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallSessionDtmfReceived(ImsCall.this, digit);
|
|
} catch (Throwable t) {
|
|
loge("callSessionDtmfReceived:: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callQualityChanged(CallQuality callQuality) {
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized (ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallQualityChanged(ImsCall.this, callQuality);
|
|
} catch (Throwable t) {
|
|
loge("callQualityChanged:: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionRtpHeaderExtensionsReceived(
|
|
@NonNull Set<RtpHeaderExtension> extensions) {
|
|
ImsCall.Listener listener;
|
|
|
|
synchronized (ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallSessionRtpHeaderExtensionsReceived(ImsCall.this, extensions);
|
|
} catch (Throwable t) {
|
|
loge("callSessionRtpHeaderExtensionsReceived:: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callSessionSendAnbrQuery(int mediaType, int direction, int bitsPerSecond) {
|
|
ImsCall.Listener listener;
|
|
|
|
logi("callSessionSendAnbrQuery in ImsCall");
|
|
synchronized (ImsCall.this) {
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallSessionSendAnbrQuery(ImsCall.this, mediaType,
|
|
direction, bitsPerSecond);
|
|
} catch (Throwable t) {
|
|
loge("callSessionSendAnbrQuery:: ", t);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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}.
|
|
*/
|
|
@VisibleForTesting
|
|
public void conferenceStateUpdated(ImsConferenceState state) {
|
|
Listener listener;
|
|
|
|
synchronized(this) {
|
|
notifyConferenceStateUpdated(state);
|
|
listener = mListener;
|
|
}
|
|
|
|
if (listener != null) {
|
|
try {
|
|
listener.onCallConferenceStateUpdated(this, state);
|
|
} catch (Throwable t) {
|
|
loge("callSessionConferenceStateUpdated :: ", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides a human-readable string representation of an update request.
|
|
*
|
|
* @param updateRequest The update request.
|
|
* @return The string representation.
|
|
*/
|
|
private String updateRequestToString(int updateRequest) {
|
|
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";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears the merge peer for this call, ensuring that the peer's connection to this call is also
|
|
* severed at the same time.
|
|
*/
|
|
private void clearMergeInfo() {
|
|
if (CONF_DBG) {
|
|
logi("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;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
private void setMergePeer(ImsCall mergePeer) {
|
|
mMergePeer = mergePeer;
|
|
mMergeHost = null;
|
|
|
|
mergePeer.mMergeHost = ImsCall.this;
|
|
mergePeer.mMergePeer = null;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public void setMergeHost(ImsCall mergeHost) {
|
|
mMergeHost = mergeHost;
|
|
mMergePeer = null;
|
|
|
|
mergeHost.mMergeHost = null;
|
|
mergeHost.mMergePeer = ImsCall.this;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
private boolean isMerging() {
|
|
return mMergePeer != null || mMergeHost != null;
|
|
}
|
|
|
|
/**
|
|
* Determines if the current call is the host of the merge.
|
|
*
|
|
* @return {@code true} if the call is the merge host.
|
|
*/
|
|
private boolean isMergeHost() {
|
|
return mMergePeer != null && mMergeHost == null;
|
|
}
|
|
|
|
/**
|
|
* Determines if the current call is the peer of the merge.
|
|
*
|
|
* @return {@code true} if the call is the merge peer.
|
|
*/
|
|
private boolean isMergePeer() {
|
|
return mMergePeer == null && mMergeHost != null;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public boolean isCallSessionMergePending() {
|
|
return mCallSessionMergePending;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
private void setCallSessionMergePending(boolean callSessionMergePending) {
|
|
mCallSessionMergePending = callSessionMergePending;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* <p>
|
|
* 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");
|
|
ImsCallProfile imsCallProfile = mCallProfile;
|
|
if (imsCallProfile != null) {
|
|
sb.append(" mCallProfile:" + imsCallProfile);
|
|
sb.append(" networkType:");
|
|
sb.append(getNetworkType());
|
|
}
|
|
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(" isVideo: ");
|
|
sb.append(isVideoCall() ? "Y" : "N");
|
|
sb.append(" wasVideo: ");
|
|
sb.append(mWasVideoCall ? "Y" : "N");
|
|
sb.append(" isWifi: ");
|
|
sb.append(isWifiCall() ? "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();
|
|
}
|
|
|
|
/**
|
|
* Updates {@link #mWasVideoCall} based on the current {@link ImsCallProfile} for the call.
|
|
*
|
|
* @param profile The current {@link ImsCallProfile} for the call.
|
|
*/
|
|
private void trackVideoStateHistory(ImsCallProfile profile) {
|
|
mWasVideoCall = mWasVideoCall || ( profile != null && profile.isVideoCall());
|
|
}
|
|
|
|
/**
|
|
* @return {@code true} if this call was a video call at some point in its life span,
|
|
* {@code false} otherwise.
|
|
*/
|
|
public boolean wasVideoCall() {
|
|
return mWasVideoCall;
|
|
}
|
|
|
|
/**
|
|
* @return {@code true} if this call is a video call, {@code false} otherwise.
|
|
*/
|
|
public boolean isVideoCall() {
|
|
synchronized(mLockObj) {
|
|
return mCallProfile != null && mCallProfile.isVideoCall();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if the current call radio access technology is over WIFI.
|
|
* Note: This depends on the RIL exposing the {@link ImsCallProfile#EXTRA_CALL_RAT_TYPE} extra.
|
|
* This method is primarily intended to be used when checking if answering an incoming audio
|
|
* call should cause a wifi video call to drop (e.g.
|
|
* {@link android.telephony.CarrierConfigManager#
|
|
* KEY_DROP_VIDEO_CALL_WHEN_ANSWERING_AUDIO_CALL_BOOL} is set).
|
|
*
|
|
* @return {@code true} if the call is over WIFI, {@code false} otherwise.
|
|
*/
|
|
public boolean isWifiCall() {
|
|
synchronized(mLockObj) {
|
|
if (mCallProfile == null) {
|
|
return false;
|
|
}
|
|
return getNetworkType() == TelephonyManager.NETWORK_TYPE_IWLAN;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines the network type for the {@link ImsCall}.
|
|
* @return The {@link TelephonyManager} {@code NETWORK_TYPE_*} code in use.
|
|
*/
|
|
public int getNetworkType() {
|
|
synchronized(mLockObj) {
|
|
if (mCallProfile == null) {
|
|
return ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN;
|
|
}
|
|
int networkType = mCallProfile.getCallExtraInt(ImsCallProfile.EXTRA_CALL_NETWORK_TYPE,
|
|
TelephonyManager.NETWORK_TYPE_UNKNOWN);
|
|
if (networkType == TelephonyManager.NETWORK_TYPE_UNKNOWN) {
|
|
// Try to look at old extras to see if the ImsService is using deprecated behavior.
|
|
String oldRatType = mCallProfile.getCallExtra(ImsCallProfile.EXTRA_CALL_RAT_TYPE);
|
|
if (TextUtils.isEmpty(oldRatType)) {
|
|
oldRatType = mCallProfile.getCallExtra(ImsCallProfile.EXTRA_CALL_RAT_TYPE_ALT);
|
|
}
|
|
try {
|
|
int oldRatTypeConverted = Integer.parseInt(oldRatType);
|
|
networkType = ServiceState.rilRadioTechnologyToNetworkType(oldRatTypeConverted);
|
|
} catch (NumberFormatException e) {
|
|
networkType = TelephonyManager.NETWORK_TYPE_UNKNOWN;
|
|
}
|
|
}
|
|
return networkType;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if the current call is a cross sim call
|
|
* Note: This depends on the RIL exposing the
|
|
* {@link ImsCallProfile#EXTRA_IS_CROSS_SIM_CALL} extra.
|
|
*
|
|
* @return {@code true} if the call is Cross SIM, {@code false} otherwise.
|
|
*/
|
|
public boolean isCrossSimCall() {
|
|
synchronized(mLockObj) {
|
|
if (mCallProfile == null) {
|
|
return false;
|
|
}
|
|
return mCallProfile.getCallExtraBoolean(
|
|
ImsCallProfile.EXTRA_IS_CROSS_SIM_CALL,
|
|
false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|