blob: ed6d223caf2c691e6658028af83f144532f1e0ba [file] [log] [blame]
/*
* Copyright (C) 2014 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.server.telecom;
import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
import android.provider.CallLog;
import android.provider.ContactsContract.Contacts;
import android.telecom.BluetoothCallQualityReport;
import android.telecom.CallAudioState;
import android.telecom.CallDiagnosticService;
import android.telecom.CallDiagnostics;
import android.telecom.CallerInfo;
import android.telecom.Conference;
import android.telecom.Connection;
import android.telecom.ConnectionService;
import android.telecom.DisconnectCause;
import android.telecom.GatewayInfo;
import android.telecom.Log;
import android.telecom.Logging.EventManager;
import android.telecom.ParcelableConference;
import android.telecom.ParcelableConnection;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.Response;
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.CallQuality;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.telephony.emergency.EmergencyNumber;
import android.telephony.ims.ImsReasonInfo;
import android.text.TextUtils;
import android.widget.Toast;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.IVideoProvider;
import com.android.internal.util.Preconditions;
import com.android.server.telecom.ui.ToastFactory;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* Encapsulates all aspects of a given phone call throughout its lifecycle, starting
* from the time the call intent was received by Telecom (vs. the time the call was
* connected etc).
*/
@VisibleForTesting
public class Call implements CreateConnectionResponse, EventManager.Loggable,
ConnectionServiceFocusManager.CallFocus {
public final static String CALL_ID_UNKNOWN = "-1";
public final static long DATA_USAGE_NOT_SET = -1;
public static final int CALL_DIRECTION_UNDEFINED = 0;
public static final int CALL_DIRECTION_OUTGOING = 1;
public static final int CALL_DIRECTION_INCOMING = 2;
public static final int CALL_DIRECTION_UNKNOWN = 3;
/** Identifies extras changes which originated from a connection service. */
public static final int SOURCE_CONNECTION_SERVICE = 1;
/** Identifies extras changes which originated from an incall service. */
public static final int SOURCE_INCALL_SERVICE = 2;
private static final int RTT_PIPE_READ_SIDE_INDEX = 0;
private static final int RTT_PIPE_WRITE_SIDE_INDEX = 1;
private static final int INVALID_RTT_REQUEST_ID = -1;
private static final char NO_DTMF_TONE = '\0';
/**
* Listener for events on the call.
*/
@VisibleForTesting
public interface Listener {
void onSuccessfulOutgoingCall(Call call, int callState);
void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause);
void onSuccessfulIncomingCall(Call call);
void onFailedIncomingCall(Call call);
void onSuccessfulUnknownCall(Call call, int callState);
void onFailedUnknownCall(Call call);
void onRingbackRequested(Call call, boolean ringbackRequested);
void onPostDialWait(Call call, String remaining);
void onPostDialChar(Call call, char nextChar);
void onConnectionCapabilitiesChanged(Call call);
void onConnectionPropertiesChanged(Call call, boolean didRttChange);
void onParentChanged(Call call);
void onChildrenChanged(Call call);
void onCannedSmsResponsesLoaded(Call call);
void onVideoCallProviderChanged(Call call);
void onCallerInfoChanged(Call call);
void onIsVoipAudioModeChanged(Call call);
void onStatusHintsChanged(Call call);
void onExtrasChanged(Call c, int source, Bundle extras);
void onExtrasRemoved(Call c, int source, List<String> keys);
void onHandleChanged(Call call);
void onCallerDisplayNameChanged(Call call);
void onCallDirectionChanged(Call call);
void onVideoStateChanged(Call call, int previousVideoState, int newVideoState);
void onTargetPhoneAccountChanged(Call call);
void onConnectionManagerPhoneAccountChanged(Call call);
void onPhoneAccountChanged(Call call);
void onConferenceableCallsChanged(Call call);
void onConferenceStateChanged(Call call, boolean isConference);
void onCdmaConferenceSwap(Call call);
boolean onCanceledViaNewOutgoingCallBroadcast(Call call, long disconnectionTimeout);
void onHoldToneRequested(Call call);
void onCallHoldFailed(Call call);
void onCallSwitchFailed(Call call);
void onConnectionEvent(Call call, String event, Bundle extras);
void onExternalCallChanged(Call call, boolean isExternalCall);
void onRttInitiationFailure(Call call, int reason);
void onRemoteRttRequest(Call call, int requestId);
void onHandoverRequested(Call call, PhoneAccountHandle handoverTo, int videoState,
Bundle extras, boolean isLegacy);
void onHandoverFailed(Call call, int error);
void onHandoverComplete(Call call);
void onBluetoothCallQualityReport(Call call, BluetoothCallQualityReport report);
void onReceivedDeviceToDeviceMessage(Call call, int messageType, int messageValue);
void onReceivedCallQualityReport(Call call, CallQuality callQuality);
void onCallerNumberVerificationStatusChanged(Call call, int callerNumberVerificationStatus);
}
public abstract static class ListenerBase implements Listener {
@Override
public void onSuccessfulOutgoingCall(Call call, int callState) {}
@Override
public void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause) {}
@Override
public void onSuccessfulIncomingCall(Call call) {}
@Override
public void onFailedIncomingCall(Call call) {}
@Override
public void onSuccessfulUnknownCall(Call call, int callState) {}
@Override
public void onFailedUnknownCall(Call call) {}
@Override
public void onRingbackRequested(Call call, boolean ringbackRequested) {}
@Override
public void onPostDialWait(Call call, String remaining) {}
@Override
public void onPostDialChar(Call call, char nextChar) {}
@Override
public void onConnectionCapabilitiesChanged(Call call) {}
@Override
public void onConnectionPropertiesChanged(Call call, boolean didRttChange) {}
@Override
public void onParentChanged(Call call) {}
@Override
public void onChildrenChanged(Call call) {}
@Override
public void onCannedSmsResponsesLoaded(Call call) {}
@Override
public void onVideoCallProviderChanged(Call call) {}
@Override
public void onCallerInfoChanged(Call call) {}
@Override
public void onIsVoipAudioModeChanged(Call call) {}
@Override
public void onStatusHintsChanged(Call call) {}
@Override
public void onExtrasChanged(Call c, int source, Bundle extras) {}
@Override
public void onExtrasRemoved(Call c, int source, List<String> keys) {}
@Override
public void onHandleChanged(Call call) {}
@Override
public void onCallerDisplayNameChanged(Call call) {}
@Override
public void onCallDirectionChanged(Call call) {}
@Override
public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {}
@Override
public void onTargetPhoneAccountChanged(Call call) {}
@Override
public void onConnectionManagerPhoneAccountChanged(Call call) {}
@Override
public void onPhoneAccountChanged(Call call) {}
@Override
public void onConferenceableCallsChanged(Call call) {}
@Override
public void onConferenceStateChanged(Call call, boolean isConference) {}
@Override
public void onCdmaConferenceSwap(Call call) {}
@Override
public boolean onCanceledViaNewOutgoingCallBroadcast(Call call, long disconnectionTimeout) {
return false;
}
@Override
public void onHoldToneRequested(Call call) {}
@Override
public void onCallHoldFailed(Call call) {}
@Override
public void onCallSwitchFailed(Call call) {}
@Override
public void onConnectionEvent(Call call, String event, Bundle extras) {}
@Override
public void onExternalCallChanged(Call call, boolean isExternalCall) {}
@Override
public void onRttInitiationFailure(Call call, int reason) {}
@Override
public void onRemoteRttRequest(Call call, int requestId) {}
@Override
public void onHandoverRequested(Call call, PhoneAccountHandle handoverTo, int videoState,
Bundle extras, boolean isLegacy) {}
@Override
public void onHandoverFailed(Call call, int error) {}
@Override
public void onHandoverComplete(Call call) {}
@Override
public void onBluetoothCallQualityReport(Call call, BluetoothCallQualityReport report) {}
@Override
public void onReceivedDeviceToDeviceMessage(Call call, int messageType, int messageValue) {}
@Override
public void onReceivedCallQualityReport(Call call, CallQuality callQuality) {}
@Override
public void onCallerNumberVerificationStatusChanged(Call call,
int callerNumberVerificationStatus) {}
}
private final CallerInfoLookupHelper.OnQueryCompleteListener mCallerInfoQueryListener =
new CallerInfoLookupHelper.OnQueryCompleteListener() {
/** ${inheritDoc} */
@Override
public void onCallerInfoQueryComplete(Uri handle, CallerInfo callerInfo) {
synchronized (mLock) {
Call.this.setCallerInfo(handle, callerInfo);
}
}
@Override
public void onContactPhotoQueryComplete(Uri handle, CallerInfo callerInfo) {
synchronized (mLock) {
Call.this.setCallerInfo(handle, callerInfo);
}
}
};
/**
* One of CALL_DIRECTION_INCOMING, CALL_DIRECTION_OUTGOING, or CALL_DIRECTION_UNKNOWN
*/
private int mCallDirection;
/**
* The post-dial digits that were dialed after the network portion of the number
*/
private String mPostDialDigits;
/**
* The secondary line number that an incoming call has been received on if the SIM subscription
* has multiple associated numbers.
*/
private String mViaNumber = "";
/**
* The wall clock time this call was created. Beyond logging and such, may also be used for
* bookkeeping and specifically for marking certain call attempts as failed attempts.
* Note: This timestamp should NOT be used for calculating call duration.
*/
private long mCreationTimeMillis;
/** The time this call was made active. */
private long mConnectTimeMillis = 0;
/**
* The time, in millis, since boot when this call was connected. This should ONLY be used when
* calculating the duration of the call.
*
* The reason for this is that the {@link SystemClock#elapsedRealtime()} is based on the
* elapsed time since the device was booted. Changes to the system clock (e.g. due to NITZ
* time sync, time zone changes user initiated clock changes) would cause a duration calculated
* based on {@link #mConnectTimeMillis} to change based on the delta in the time.
* Using the {@link SystemClock#elapsedRealtime()} ensures that changes to the wall clock do
* not impact the call duration.
*/
private long mConnectElapsedTimeMillis = 0;
/** The wall clock time this call was disconnected. */
private long mDisconnectTimeMillis = 0;
/**
* The elapsed time since boot when this call was disconnected. Recorded as the
* {@link SystemClock#elapsedRealtime()}. This ensures that the call duration is not impacted
* by changes in the wall time clock.
*/
private long mDisconnectElapsedTimeMillis = 0;
/** The gateway information associated with this call. This stores the original call handle
* that the user is attempting to connect to via the gateway, the actual handle to dial in
* order to connect the call via the gateway, as well as the package name of the gateway
* service. */
private GatewayInfo mGatewayInfo;
private PhoneAccountHandle mConnectionManagerPhoneAccountHandle;
private PhoneAccountHandle mTargetPhoneAccountHandle;
private PhoneAccountHandle mRemotePhoneAccountHandle;
private UserHandle mInitiatingUser;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final List<Call> mConferenceableCalls = new ArrayList<>();
/** The state of the call. */
private int mState;
/** The handle with which to establish this call. */
private Uri mHandle;
/** The participants with which to establish adhoc conference call */
private List<Uri> mParticipants;
/**
* The presentation requirements for the handle. See {@link TelecomManager} for valid values.
*/
private int mHandlePresentation;
/**
* The verification status for an incoming call's number.
*/
private @Connection.VerificationStatus int mCallerNumberVerificationStatus;
/** The caller display name (CNAP) set by the connection service. */
private String mCallerDisplayName;
/**
* The presentation requirements for the handle. See {@link TelecomManager} for valid values.
*/
private int mCallerDisplayNamePresentation;
/**
* The connection service which is attempted or already connecting this call.
*/
private ConnectionServiceWrapper mConnectionService;
private boolean mIsEmergencyCall;
// The Call is considered an emergency call for testing, but will not actually connect to
// emergency services.
private boolean mIsTestEmergencyCall;
private boolean mSpeakerphoneOn;
private boolean mIsDisconnectingChildCall = false;
/**
* Tracks the video states which were applicable over the duration of a call.
* See {@link VideoProfile} for a list of valid video states.
* <p>
* Video state history is tracked when the call is active, and when a call is rejected or
* missed.
*/
private int mVideoStateHistory;
private int mVideoState;
/**
* Disconnect cause for the call. Only valid if the state of the call is STATE_DISCONNECTED.
* See {@link android.telecom.DisconnectCause}.
*/
private DisconnectCause mDisconnectCause = new DisconnectCause(DisconnectCause.UNKNOWN);
/**
* Override the disconnect cause set by the connection service. Used for audio processing and
* simulated ringing calls as well as the condition when an emergency call is ended due to
* an emergency call being placed.
*/
private DisconnectCause mOverrideDisconnectCause = new DisconnectCause(DisconnectCause.UNKNOWN);
private Bundle mIntentExtras = new Bundle();
/**
* The {@link Intent} which originally created this call. Only populated when we are putting a
* call into a pending state and need to pick up initiation of the call later.
*/
private Intent mOriginalCallIntent = null;
/** Set of listeners on this call.
*
* ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
* load factor before resizing, 1 means we only expect a single thread to
* access the map so make only a single shard
*/
private final Set<Listener> mListeners = Collections.newSetFromMap(
new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
private CreateConnectionProcessor mCreateConnectionProcessor;
/** Caller information retrieved from the latest contact query. */
private CallerInfo mCallerInfo;
/** The latest token used with a contact info query. */
private int mQueryToken = 0;
/** Whether this call is requesting that Telecom play the ringback tone on its behalf. */
private boolean mRingbackRequested = false;
/** Whether this call is requesting to be silently ringing. */
private boolean mSilentRingingRequested = false;
/** Whether direct-to-voicemail query is pending. */
private boolean mDirectToVoicemailQueryPending;
private int mConnectionCapabilities;
private int mConnectionProperties;
private int mSupportedAudioRoutes = CallAudioState.ROUTE_ALL;
private boolean mIsConference = false;
private boolean mHadChildren = false;
private final boolean mShouldAttachToExistingConnection;
private Call mParentCall = null;
private List<Call> mChildCalls = new LinkedList<>();
/** Set of text message responses allowed for this call, if applicable. */
private List<String> mCannedSmsResponses = Collections.EMPTY_LIST;
/** Whether an attempt has been made to load the text message responses. */
private boolean mCannedSmsResponsesLoadingStarted = false;
private IVideoProvider mVideoProvider;
private VideoProviderProxy mVideoProviderProxy;
private boolean mIsVoipAudioMode;
private StatusHints mStatusHints;
private Bundle mExtras;
private final ConnectionServiceRepository mRepository;
private final Context mContext;
private final CallsManager mCallsManager;
private final ClockProxy mClockProxy;
private final ToastFactory mToastFactory;
private final TelecomSystem.SyncRoot mLock;
private final String mId;
private String mConnectionId;
private Analytics.CallInfo mAnalytics = new Analytics.CallInfo();
private char mPlayingDtmfTone;
private boolean mWasConferencePreviouslyMerged = false;
private boolean mWasHighDefAudio = false;
private boolean mWasWifi = false;
private boolean mWasVolte = false;
// For conferences which support merge/swap at their level, we retain a notion of an active
// call. This is used for BluetoothPhoneService. In order to support hold/merge, it must have
// the notion of the current "active" call within the conference call. This maintains the
// "active" call and switches every time the user hits "swap".
private Call mConferenceLevelActiveCall = null;
private boolean mIsLocallyDisconnecting = false;
/**
* Tracks the current call data usage as reported by the video provider.
*/
private long mCallDataUsage = DATA_USAGE_NOT_SET;
private boolean mIsWorkCall;
/**
* Tracks whether this {@link Call}'s {@link #getTargetPhoneAccount()} has
* {@link PhoneAccount#EXTRA_PLAY_CALL_RECORDING_TONE} set.
*/
private boolean mUseCallRecordingTone;
// Set to true once the NewOutgoingCallIntentBroadcast comes back and is processed.
private boolean mIsNewOutgoingCallIntentBroadcastDone = false;
/**
* Indicates whether the call is remotely held. A call is considered remotely held when
* {@link #onConnectionEvent(String)} receives the {@link Connection#EVENT_ON_HOLD_TONE_START}
* event.
*/
private boolean mIsRemotelyHeld = false;
/**
* Indicates whether the {@link PhoneAccount} associated with this call is self-managed.
* See {@link PhoneAccount#CAPABILITY_SELF_MANAGED} for more information.
*/
private boolean mIsSelfManaged = false;
/**
* Indicates whether the {@link PhoneAccount} associated with an self-managed call want to
* expose the call to an {@link android.telecom.InCallService} which declares the metadata
* {@link TelecomManager#METADATA_INCLUDE_SELF_MANAGED_CALLS},
* For calls that {@link #mIsSelfManaged} is {@code false}, this value should be {@code false}
* as well.
*/
private boolean mVisibleToInCallService = false;
/**
* Indicates whether the {@link PhoneAccount} associated with this call supports video calling.
* {@code True} if the phone account supports video calling, {@code false} otherwise.
*/
private boolean mIsVideoCallingSupportedByPhoneAccount = false;
/**
* Indicates whether or not this call can be pulled if it is an external call. If true, respect
* the Connection Capability set by the ConnectionService. If false, override the capability
* set and always remove the ability to pull this external call.
*
* See {@link #setIsPullExternalCallSupported(boolean)}
*/
private boolean mIsPullExternalCallSupported = true;
private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
/**
* For {@link Connection}s or {@link android.telecom.Conference}s added via a ConnectionManager
* using the {@link android.telecom.ConnectionService#addExistingConnection(PhoneAccountHandle,
* Connection)} or {@link android.telecom.ConnectionService#addConference(Conference)},
* indicates the ID of this call as it was referred to by the {@code ConnectionService} which
* originally created it.
*
* See {@link Connection#EXTRA_ORIGINAL_CONNECTION_ID} for more information.
*/
private String mOriginalConnectionId;
/**
* Two pairs of {@link android.os.ParcelFileDescriptor}s that handle RTT text communication
* between the in-call app and the connection service. If both non-null, this call should be
* treated as an RTT call.
* Each array should be of size 2. First one is the read side and the second one is the write
* side.
*/
private ParcelFileDescriptor[] mInCallToConnectionServiceStreams;
private ParcelFileDescriptor[] mConnectionServiceToInCallStreams;
/**
* True if we're supposed to start this call with RTT, either due to the settings switch or due
* to an extra.
*/
private boolean mDidRequestToStartWithRtt = false;
/**
* Integer constant from {@link android.telecom.Call.RttCall}. Describes the current RTT mode.
*/
private int mRttMode;
/**
* True if the call was ever an RTT call.
*/
private boolean mWasEverRtt = false;
/**
* Integer indicating the remote RTT request ID that is pending a response from the user.
*/
private int mPendingRttRequestId = INVALID_RTT_REQUEST_ID;
/**
* When a call handover has been initiated via {@link #requestHandover(PhoneAccountHandle,
* int, Bundle, boolean)}, contains the call which this call is being handed over to.
*/
private Call mHandoverDestinationCall = null;
/**
* When a call handover has been initiated via {@link #requestHandover(PhoneAccountHandle,
* int, Bundle, boolean)}, contains the call which this call is being handed over from.
*/
private Call mHandoverSourceCall = null;
/**
* The user-visible app name of the app that requested for this call to be put into the
* AUDIO_PROCESSING state. Used to display a notification to the user.
*/
private CharSequence mAudioProcessingRequestingApp = null;
/**
* Indicates the current state of this call if it is in the process of a handover.
*/
private int mHandoverState = HandoverState.HANDOVER_NONE;
/**
* Indicates whether this call is using one of the
* {@link com.android.server.telecom.callfiltering.CallFilter} modules.
*/
private boolean mIsUsingCallFiltering = false;
/**
* Indicates whether or not this call has been active before. This is helpful in detecting
* situations where we have moved into {@link CallState#SIMULATED_RINGING} or
* {@link CallState#AUDIO_PROCESSING} again after being active. If a call has moved into one
* of these states again after being active and the user dials an emergency call, we want to
* log these calls normally instead of considering them MISSED. If the emergency call was
* dialed during initial screening however, we want to treat those calls as MISSED (because the
* user never got the chance to explicitly reject).
*/
private boolean mHasGoneActiveBefore = false;
/**
* Indicates the package name of the {@link android.telecom.CallScreeningService} which should
* be sent the {@link android.telecom.TelecomManager#ACTION_POST_CALL} intent upon disconnection
* of a call.
*/
private String mPostCallPackageName;
/**
* Call missed information code.
*/
@CallLog.Calls.MissedReason private long mMissedReason;
/**
* Time that this call start ringing or simulated ringing.
*/
private long mStartRingTime;
/**
* The package name of the call screening service that silence this call. If the call is not
* silenced, this field will be null.
*/
private CharSequence mCallScreeningAppName;
/**
* The component name of the call screening service that silence this call. If the call is not
* silenced, this field will be null.
*/
private String mCallScreeningComponentName;
/**
* When {@code true} indicates this call originated from a SIM-based {@link PhoneAccount}.
* A sim-based {@link PhoneAccount} is one with {@link PhoneAccount#CAPABILITY_SIM_SUBSCRIPTION}
* set.
*/
private boolean mIsSimCall;
/**
* Set to {@code true} if we received a valid response ({@code null} or otherwise) from
* the {@link CallDiagnostics#onCallDisconnected(ImsReasonInfo)} or
* {@link CallDiagnostics#onCallDisconnected(int, int)} calls. This is used to detect a timeout
* when awaiting a response from the call diagnostic service.
*/
private boolean mReceivedCallDiagnosticPostCallResponse = false;
/**
* {@link CompletableFuture} used to delay posting disconnection and removal to a call until
* after a {@link CallDiagnosticService} is able to handle the disconnection and provide a
* disconnect message via {@link CallDiagnostics#onCallDisconnected(ImsReasonInfo)} or
* {@link CallDiagnostics#onCallDisconnected(int, int)}.
*/
private CompletableFuture<Boolean> mDisconnectFuture;
/**
* Persists the specified parameters and initializes the new instance.
* @param context The context.
* @param repository The connection service repository.
* @param handle The handle to dial.
* @param gatewayInfo Gateway information to use for the call.
* @param connectionManagerPhoneAccountHandle Account to use for the service managing the call.
* This account must be one that was registered with the
* {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} flag.
* @param targetPhoneAccountHandle Account information to use for the call. This account must be
* one that was registered with the {@link PhoneAccount#CAPABILITY_CALL_PROVIDER} flag.
* @param callDirection one of CALL_DIRECTION_INCOMING, CALL_DIRECTION_OUTGOING,
* or CALL_DIRECTION_UNKNOWN.
* @param shouldAttachToExistingConnection Set to true to attach the call to an existing
* @param clockProxy
*/
public Call(
String callId,
Context context,
CallsManager callsManager,
TelecomSystem.SyncRoot lock,
ConnectionServiceRepository repository,
PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
Uri handle,
GatewayInfo gatewayInfo,
PhoneAccountHandle connectionManagerPhoneAccountHandle,
PhoneAccountHandle targetPhoneAccountHandle,
int callDirection,
boolean shouldAttachToExistingConnection,
boolean isConference,
ClockProxy clockProxy,
ToastFactory toastFactory) {
this(callId, context, callsManager, lock, repository, phoneNumberUtilsAdapter,
handle, null, gatewayInfo, connectionManagerPhoneAccountHandle,
targetPhoneAccountHandle, callDirection, shouldAttachToExistingConnection,
isConference, clockProxy, toastFactory);
}
public Call(
String callId,
Context context,
CallsManager callsManager,
TelecomSystem.SyncRoot lock,
ConnectionServiceRepository repository,
PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
Uri handle,
List<Uri> participants,
GatewayInfo gatewayInfo,
PhoneAccountHandle connectionManagerPhoneAccountHandle,
PhoneAccountHandle targetPhoneAccountHandle,
int callDirection,
boolean shouldAttachToExistingConnection,
boolean isConference,
ClockProxy clockProxy,
ToastFactory toastFactory) {
mId = callId;
mConnectionId = callId;
mState = (isConference && callDirection != CALL_DIRECTION_INCOMING &&
callDirection != CALL_DIRECTION_OUTGOING) ?
CallState.ACTIVE : CallState.NEW;
mContext = context;
mCallsManager = callsManager;
mLock = lock;
mRepository = repository;
mPhoneNumberUtilsAdapter = phoneNumberUtilsAdapter;
setHandle(handle);
mParticipants = participants;
mPostDialDigits = handle != null
? PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart()) : "";
mGatewayInfo = gatewayInfo;
setConnectionManagerPhoneAccount(connectionManagerPhoneAccountHandle);
setTargetPhoneAccount(targetPhoneAccountHandle);
mCallDirection = callDirection;
mIsConference = isConference;
mShouldAttachToExistingConnection = shouldAttachToExistingConnection
|| callDirection == CALL_DIRECTION_INCOMING;
maybeLoadCannedSmsResponses();
mClockProxy = clockProxy;
mToastFactory = toastFactory;
mCreationTimeMillis = mClockProxy.currentTimeMillis();
mMissedReason = MISSED_REASON_NOT_MISSED;
mStartRingTime = 0;
}
/**
* Persists the specified parameters and initializes the new instance.
* @param context The context.
* @param repository The connection service repository.
* @param handle The handle to dial.
* @param gatewayInfo Gateway information to use for the call.
* @param connectionManagerPhoneAccountHandle Account to use for the service managing the call.
* This account must be one that was registered with the
* {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} flag.
* @param targetPhoneAccountHandle Account information to use for the call. This account must be
* one that was registered with the {@link PhoneAccount#CAPABILITY_CALL_PROVIDER} flag.
* @param callDirection one of CALL_DIRECTION_INCOMING, CALL_DIRECTION_OUTGOING,
* or CALL_DIRECTION_UNKNOWN
* @param shouldAttachToExistingConnection Set to true to attach the call to an existing
* connection, regardless of whether it's incoming or outgoing.
* @param connectTimeMillis The connection time of the call.
* @param clockProxy
*/
Call(
String callId,
Context context,
CallsManager callsManager,
TelecomSystem.SyncRoot lock,
ConnectionServiceRepository repository,
PhoneNumberUtilsAdapter phoneNumberUtilsAdapter,
Uri handle,
GatewayInfo gatewayInfo,
PhoneAccountHandle connectionManagerPhoneAccountHandle,
PhoneAccountHandle targetPhoneAccountHandle,
int callDirection,
boolean shouldAttachToExistingConnection,
boolean isConference,
long connectTimeMillis,
long connectElapsedTimeMillis,
ClockProxy clockProxy,
ToastFactory toastFactory) {
this(callId, context, callsManager, lock, repository,
phoneNumberUtilsAdapter, handle, gatewayInfo,
connectionManagerPhoneAccountHandle, targetPhoneAccountHandle, callDirection,
shouldAttachToExistingConnection, isConference, clockProxy, toastFactory);
mConnectTimeMillis = connectTimeMillis;
mConnectElapsedTimeMillis = connectElapsedTimeMillis;
mAnalytics.setCallStartTime(connectTimeMillis);
}
public void addListener(Listener listener) {
mListeners.add(listener);
}
public void removeListener(Listener listener) {
if (listener != null) {
mListeners.remove(listener);
}
}
public void initAnalytics() {
initAnalytics(null, null);
}
public void initAnalytics(String callingPackage, String extraCreationLogs) {
int analyticsDirection;
switch (mCallDirection) {
case CALL_DIRECTION_OUTGOING:
analyticsDirection = Analytics.OUTGOING_DIRECTION;
break;
case CALL_DIRECTION_INCOMING:
analyticsDirection = Analytics.INCOMING_DIRECTION;
break;
case CALL_DIRECTION_UNKNOWN:
case CALL_DIRECTION_UNDEFINED:
default:
analyticsDirection = Analytics.UNKNOWN_DIRECTION;
}
mAnalytics = Analytics.initiateCallAnalytics(mId, analyticsDirection);
mAnalytics.setCallIsEmergency(mIsEmergencyCall);
Log.addEvent(this, LogUtils.Events.CREATED, callingPackage + ";" + extraCreationLogs);
}
public Analytics.CallInfo getAnalytics() {
return mAnalytics;
}
public void destroy() {
// We should not keep these bitmaps around because the Call objects may be held for logging
// purposes.
// TODO: Make a container object that only stores the information we care about for Logging.
if (mCallerInfo != null) {
mCallerInfo.cachedPhotoIcon = null;
mCallerInfo.cachedPhoto = null;
}
closeRttStreams();
Log.addEvent(this, LogUtils.Events.DESTROYED);
}
private void closeRttStreams() {
if (mConnectionServiceToInCallStreams != null) {
for (ParcelFileDescriptor fd : mConnectionServiceToInCallStreams) {
if (fd != null) {
try {
fd.close();
} catch (IOException e) {
// ignore
}
}
}
}
if (mInCallToConnectionServiceStreams != null) {
for (ParcelFileDescriptor fd : mInCallToConnectionServiceStreams) {
if (fd != null) {
try {
fd.close();
} catch (IOException e) {
// ignore
}
}
}
}
}
/** {@inheritDoc} */
@Override
public String toString() {
return String.format(Locale.US, "[Call id=%s, state=%s, tpac=%s, cmgr=%s, handle=%s, "
+ "vidst=%s, childs(%d), has_parent(%b), cap=%s, prop=%s]",
mId,
CallState.toString(getParcelableCallState()),
getTargetPhoneAccount(),
getConnectionManagerPhoneAccount(),
Log.piiHandle(mHandle),
getVideoStateDescription(getVideoState()),
getChildCalls().size(),
getParentCall() != null,
Connection.capabilitiesToStringShort(getConnectionCapabilities()),
Connection.propertiesToStringShort(getConnectionProperties()));
}
@Override
public String getDescription() {
StringBuilder s = new StringBuilder();
if (isSelfManaged()) {
s.append("SelfMgd Call");
} else if (isExternalCall()) {
s.append("External Call");
} else {
s.append("Call");
}
s.append(getId());
s.append(" [");
s.append(SimpleDateFormat.getDateTimeInstance().format(new Date(getCreationTimeMillis())));
s.append("]");
s.append(isIncoming() ? "(MT - incoming)" : "(MO - outgoing)");
s.append("\n\t");
PhoneAccountHandle targetPhoneAccountHandle = getTargetPhoneAccount();
PhoneAccountHandle remotePhoneAccountHandle = getRemotePhoneAccountHandle();
PhoneAccountHandle connectionMgrAccountHandle = getConnectionManagerPhoneAccount();
PhoneAccountHandle delegatePhoneAccountHandle = getDelegatePhoneAccountHandle();
boolean isTargetSameAsRemote = targetPhoneAccountHandle != null
&& targetPhoneAccountHandle.equals(remotePhoneAccountHandle);
if (Objects.equals(delegatePhoneAccountHandle, targetPhoneAccountHandle)) {
s.append(">>>");
}
s.append("Target");
s.append(" PhoneAccount: ");
if (targetPhoneAccountHandle != null) {
s.append(targetPhoneAccountHandle);
s.append(" (");
s.append(getTargetPhoneAccountLabel());
s.append(")");
if (isTargetSameAsRemote) {
s.append("(remote)");
}
} else {
s.append("not set");
}
if (!isTargetSameAsRemote && remotePhoneAccountHandle != null) {
// This is a RARE case and will likely not be seen in practice but it is possible.
if (delegatePhoneAccountHandle.equals(remotePhoneAccountHandle)) {
s.append("\n\t>>>Remote PhoneAccount: ");
} else {
s.append("\n\tRemote PhoneAccount: ");
}
s.append(remotePhoneAccountHandle);
}
if (connectionMgrAccountHandle != null) {
if (delegatePhoneAccountHandle.equals(connectionMgrAccountHandle)) {
s.append("\n\t>>>Conn mgr: ");
} else {
s.append("\n\tConn mgr: ");
}
s.append(connectionMgrAccountHandle);
}
s.append("\n\tTo address: ");
s.append(Log.piiHandle(getHandle()));
if (isIncoming()) {
switch (mCallerNumberVerificationStatus) {
case Connection.VERIFICATION_STATUS_FAILED:
s.append(" Verstat: fail");
break;
case Connection.VERIFICATION_STATUS_NOT_VERIFIED:
s.append(" Verstat: not");
break;
case Connection.VERIFICATION_STATUS_PASSED:
s.append(" Verstat: pass");
break;
}
}
s.append(" Presentation: ");
switch (getHandlePresentation()) {
case TelecomManager.PRESENTATION_ALLOWED:
s.append("Allowed");
break;
case TelecomManager.PRESENTATION_PAYPHONE:
s.append("Payphone");
break;
case TelecomManager.PRESENTATION_RESTRICTED:
s.append("Restricted");
break;
case TelecomManager.PRESENTATION_UNKNOWN:
s.append("Unknown");
break;
default:
s.append("<undefined>");
}
s.append("\n");
return s.toString();
}
/**
* Builds a debug-friendly description string for a video state.
* <p>
* A = audio active, T = video transmission active, R = video reception active, P = video
* paused.
*
* @param videoState The video state.
* @return A string indicating which bits are set in the video state.
*/
private String getVideoStateDescription(int videoState) {
StringBuilder sb = new StringBuilder();
sb.append("A");
if (VideoProfile.isTransmissionEnabled(videoState)) {
sb.append("T");
}
if (VideoProfile.isReceptionEnabled(videoState)) {
sb.append("R");
}
if (VideoProfile.isPaused(videoState)) {
sb.append("P");
}
return sb.toString();
}
@Override
public ConnectionServiceFocusManager.ConnectionServiceFocus getConnectionServiceWrapper() {
return mConnectionService;
}
@VisibleForTesting
public int getState() {
return mState;
}
/**
* Similar to {@link #getState()}, except will return {@link CallState#DISCONNECTING} if the
* call is locally disconnecting. This is the call state which is reported to the
* {@link android.telecom.InCallService}s when a call is parcelled.
* @return The parcelable call state.
*/
public int getParcelableCallState() {
if (isLocallyDisconnecting() &&
(mState != android.telecom.Call.STATE_DISCONNECTED)) {
return CallState.DISCONNECTING;
}
return mState;
}
/**
* Determines if this {@link Call} can receive call focus via the
* {@link ConnectionServiceFocusManager}.
* Only top-level calls and non-external calls are eligible.
* @return {@code true} if this call is focusable, {@code false} otherwise.
*/
@Override
public boolean isFocusable() {
boolean isChild = getParentCall() != null;
return !isChild && !isExternalCall();
}
private boolean shouldContinueProcessingAfterDisconnect() {
// Stop processing once the call is active.
if (!CreateConnectionTimeout.isCallBeingPlaced(this)) {
return false;
}
// Only Redial a Call in the case of it being an Emergency Call.
if(!isEmergencyCall()) {
return false;
}
// Make sure that there are additional connection services to process.
if (mCreateConnectionProcessor == null
|| !mCreateConnectionProcessor.isProcessingComplete()
|| !mCreateConnectionProcessor.hasMorePhoneAccounts()) {
return false;
}
if (mDisconnectCause == null) {
return false;
}
// Continue processing if the current attempt failed or timed out.
return mDisconnectCause.getCode() == DisconnectCause.ERROR ||
mCreateConnectionProcessor.isCallTimedOut();
}
/**
* Returns the unique ID for this call as it exists in Telecom.
* @return The call ID.
*/
public String getId() {
return mId;
}
/**
* Returns the unique ID for this call (see {@link #getId}) along with an attempt indicator that
* iterates based on attempts to establish a {@link Connection} using createConnectionProcessor.
* @return The call ID with an appended attempt id.
*/
public String getConnectionId() {
if(mCreateConnectionProcessor != null) {
mConnectionId = mId + "_" +
String.valueOf(mCreateConnectionProcessor.getConnectionAttempt());
return mConnectionId;
} else {
return mConnectionId;
}
}
/**
* Handles an incoming overridden disconnect message for this call.
*
* We only care if the disconnect is handled via a future.
* @param message the overridden disconnect message.
*/
public void handleOverrideDisconnectMessage(@Nullable CharSequence message) {
Log.i(this, "handleOverrideDisconnectMessage; callid=%s, msg=%s", getId(), message);
if (isDisconnectHandledViaFuture()) {
mReceivedCallDiagnosticPostCallResponse = true;
if (message != null) {
Log.addEvent(this, LogUtils.Events.OVERRIDE_DISCONNECT_MESSAGE, message);
// Replace the existing disconnect cause in this call
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.ERROR, message,
message, null));
}
mDisconnectFuture.complete(true);
} else {
Log.w(this, "handleOverrideDisconnectMessage; callid=%s - got override when unbound",
getId());
}
}
/**
* Sets the call state. Although there exists the notion of appropriate state transitions
* (see {@link CallState}), in practice those expectations break down when cellular systems
* misbehave and they do this very often. The result is that we do not enforce state transitions
* and instead keep the code resilient to unexpected state changes.
* @return true indicates if setState succeeded in setting the state to newState,
* else it is failed, and the call is still in its original state.
*/
public boolean setState(int newState, String tag) {
if (mState != newState) {
Log.v(this, "setState %s -> %s", CallState.toString(mState),
CallState.toString(newState));
if (newState == CallState.DISCONNECTED && shouldContinueProcessingAfterDisconnect()) {
Log.w(this, "continuing processing disconnected call with another service");
mCreateConnectionProcessor.continueProcessingIfPossible(this, mDisconnectCause);
return false;
} else if (newState == CallState.ANSWERED && mState == CallState.ACTIVE) {
Log.w(this, "setState %s -> %s; call already active.", CallState.toString(mState),
CallState.toString(newState));
return false;
}
updateVideoHistoryViaState(mState, newState);
mState = newState;
maybeLoadCannedSmsResponses();
if (mState == CallState.ACTIVE || mState == CallState.ON_HOLD) {
if (mConnectTimeMillis == 0) {
// We check to see if mConnectTime is already set to prevent the
// call from resetting active time when it goes in and out of
// ACTIVE/ON_HOLD
mConnectTimeMillis = mClockProxy.currentTimeMillis();
mConnectElapsedTimeMillis = mClockProxy.elapsedRealtime();
mAnalytics.setCallStartTime(mConnectTimeMillis);
}
// We're clearly not disconnected, so reset the disconnected time.
mDisconnectTimeMillis = 0;
mDisconnectElapsedTimeMillis = 0;
mHasGoneActiveBefore = true;
} else if (mState == CallState.DISCONNECTED) {
mDisconnectTimeMillis = mClockProxy.currentTimeMillis();
mDisconnectElapsedTimeMillis = mClockProxy.elapsedRealtime();
mAnalytics.setCallEndTime(mDisconnectTimeMillis);
setLocallyDisconnecting(false);
fixParentAfterDisconnect();
}
// Log the state transition event
String event = null;
Object data = null;
switch (newState) {
case CallState.ACTIVE:
event = LogUtils.Events.SET_ACTIVE;
break;
case CallState.CONNECTING:
event = LogUtils.Events.SET_CONNECTING;
break;
case CallState.DIALING:
event = LogUtils.Events.SET_DIALING;
break;
case CallState.PULLING:
event = LogUtils.Events.SET_PULLING;
break;
case CallState.DISCONNECTED:
event = LogUtils.Events.SET_DISCONNECTED;
data = getDisconnectCause();
break;
case CallState.DISCONNECTING:
event = LogUtils.Events.SET_DISCONNECTING;
break;
case CallState.ON_HOLD:
event = LogUtils.Events.SET_HOLD;
break;
case CallState.SELECT_PHONE_ACCOUNT:
event = LogUtils.Events.SET_SELECT_PHONE_ACCOUNT;
break;
case CallState.RINGING:
event = LogUtils.Events.SET_RINGING;
break;
case CallState.ANSWERED:
event = LogUtils.Events.SET_ANSWERED;
break;
case CallState.AUDIO_PROCESSING:
event = LogUtils.Events.SET_AUDIO_PROCESSING;
break;
case CallState.SIMULATED_RINGING:
event = LogUtils.Events.SET_SIMULATED_RINGING;
break;
}
if (event != null) {
// The string data should be just the tag.
String stringData = tag;
if (data != null) {
// If data exists, add it to tag. If no tag, just use data.toString().
stringData = stringData == null ? data.toString() : stringData + "> " + data;
}
Log.addEvent(this, event, stringData);
}
int statsdDisconnectCause = (newState == CallState.DISCONNECTED) ?
getDisconnectCause().getCode() : DisconnectCause.UNKNOWN;
TelecomStatsLog.write(TelecomStatsLog.CALL_STATE_CHANGED, newState,
statsdDisconnectCause, isSelfManaged(), isExternalCall());
}
return true;
}
void setRingbackRequested(boolean ringbackRequested) {
mRingbackRequested = ringbackRequested;
for (Listener l : mListeners) {
l.onRingbackRequested(this, mRingbackRequested);
}
}
boolean isRingbackRequested() {
return mRingbackRequested;
}
public void setSilentRingingRequested(boolean silentRingingRequested) {
mSilentRingingRequested = silentRingingRequested;
Bundle bundle = new Bundle();
bundle.putBoolean(android.telecom.Call.EXTRA_SILENT_RINGING_REQUESTED,
silentRingingRequested);
putExtras(SOURCE_CONNECTION_SERVICE, bundle);
}
public boolean isSilentRingingRequested() {
return mSilentRingingRequested;
}
@VisibleForTesting
public boolean isConference() {
return mIsConference;
}
/**
* @return {@code true} if this call had children at some point, {@code false} otherwise.
*/
public boolean hadChildren() {
return mHadChildren;
}
public Uri getHandle() {
return mHandle;
}
public List<Uri> getParticipants() {
return mParticipants;
}
public boolean isAdhocConferenceCall() {
return mIsConference &&
(mCallDirection == CALL_DIRECTION_OUTGOING ||
mCallDirection == CALL_DIRECTION_INCOMING);
}
public String getPostDialDigits() {
return mPostDialDigits;
}
public void clearPostDialDigits() {
mPostDialDigits = null;
}
public String getViaNumber() {
return mViaNumber;
}
public void setViaNumber(String viaNumber) {
// If at any point the via number is not empty throughout the call, save that via number.
if (!TextUtils.isEmpty(viaNumber)) {
mViaNumber = viaNumber;
}
}
public int getHandlePresentation() {
return mHandlePresentation;
}
public void setCallerNumberVerificationStatus(
@Connection.VerificationStatus int callerNumberVerificationStatus) {
mCallerNumberVerificationStatus = callerNumberVerificationStatus;
mListeners.forEach(l -> l.onCallerNumberVerificationStatusChanged(this,
callerNumberVerificationStatus));
}
public @Connection.VerificationStatus int getCallerNumberVerificationStatus() {
return mCallerNumberVerificationStatus;
}
void setHandle(Uri handle) {
setHandle(handle, TelecomManager.PRESENTATION_ALLOWED);
}
public void setHandle(Uri handle, int presentation) {
if (!Objects.equals(handle, mHandle) || presentation != mHandlePresentation) {
mHandlePresentation = presentation;
if (mHandlePresentation == TelecomManager.PRESENTATION_RESTRICTED ||
mHandlePresentation == TelecomManager.PRESENTATION_UNKNOWN) {
mHandle = null;
} else {
mHandle = handle;
if (mHandle != null && !PhoneAccount.SCHEME_VOICEMAIL.equals(mHandle.getScheme())
&& TextUtils.isEmpty(mHandle.getSchemeSpecificPart())) {
// If the number is actually empty, set it to null, unless this is a
// SCHEME_VOICEMAIL uri which always has an empty number.
mHandle = null;
}
}
// Let's not allow resetting of the emergency flag. Once a call becomes an emergency
// call, it will remain so for the rest of it's lifetime.
if (!mIsEmergencyCall) {
try {
mIsEmergencyCall = mHandle != null &&
getTelephonyManager().isEmergencyNumber(
mHandle.getSchemeSpecificPart());
} catch (IllegalStateException ise) {
Log.e(this, ise, "setHandle: can't determine if number is emergency");
mIsEmergencyCall = false;
} catch (RuntimeException r) {
Log.e(this, r, "setHandle: can't determine if number is emergency");
mIsEmergencyCall = false;
}
mAnalytics.setCallIsEmergency(mIsEmergencyCall);
}
if (!mIsTestEmergencyCall) {
mIsTestEmergencyCall = mHandle != null &&
isTestEmergencyCall(mHandle.getSchemeSpecificPart());
}
startCallerInfoLookup();
for (Listener l : mListeners) {
l.onHandleChanged(this);
}
}
}
private boolean isTestEmergencyCall(String number) {
try {
Map<Integer, List<EmergencyNumber>> eMap =
getTelephonyManager().getEmergencyNumberList();
return eMap.values().stream().flatMap(Collection::stream)
.anyMatch(eNumber ->
eNumber.isFromSources(EmergencyNumber.EMERGENCY_NUMBER_SOURCE_TEST) &&
number.equals(eNumber.getNumber()));
} catch (IllegalStateException ise) {
return false;
} catch (RuntimeException r) {
return false;
}
}
public String getCallerDisplayName() {
return mCallerDisplayName;
}
public int getCallerDisplayNamePresentation() {
return mCallerDisplayNamePresentation;
}
void setCallerDisplayName(String callerDisplayName, int presentation) {
if (!TextUtils.equals(callerDisplayName, mCallerDisplayName) ||
presentation != mCallerDisplayNamePresentation) {
mCallerDisplayName = callerDisplayName;
mCallerDisplayNamePresentation = presentation;
for (Listener l : mListeners) {
l.onCallerDisplayNameChanged(this);
}
}
}
public String getName() {
return mCallerInfo == null ? null : mCallerInfo.getName();
}
public String getPhoneNumber() {
return mCallerInfo == null ? null : mCallerInfo.getPhoneNumber();
}
public Bitmap getPhotoIcon() {
return mCallerInfo == null ? null : mCallerInfo.cachedPhotoIcon;
}
public Drawable getPhoto() {
return mCallerInfo == null ? null : mCallerInfo.cachedPhoto;
}
/**
* @param cause The reason for the disconnection, represented by
* {@link android.telecom.DisconnectCause}.
*/
public void setDisconnectCause(DisconnectCause cause) {
// TODO: Consider combining this method with a setDisconnected() method that is totally
// separate from setState.
if (mOverrideDisconnectCause.getCode() != DisconnectCause.UNKNOWN) {
cause = new DisconnectCause(mOverrideDisconnectCause.getCode(),
TextUtils.isEmpty(mOverrideDisconnectCause.getLabel()) ?
cause.getLabel() : mOverrideDisconnectCause.getLabel(),
(mOverrideDisconnectCause.getDescription() == null) ?
cause.getDescription() :mOverrideDisconnectCause.getDescription(),
TextUtils.isEmpty(mOverrideDisconnectCause.getReason()) ?
cause.getReason() : mOverrideDisconnectCause.getReason(),
(mOverrideDisconnectCause.getTone() == 0) ?
cause.getTone() : mOverrideDisconnectCause.getTone());
}
mAnalytics.setCallDisconnectCause(cause);
mDisconnectCause = cause;
}
public void setOverrideDisconnectCauseCode(DisconnectCause overrideDisconnectCause) {
mOverrideDisconnectCause = overrideDisconnectCause;
}
public DisconnectCause getDisconnectCause() {
return mDisconnectCause;
}
/**
* @return {@code true} if this is an outgoing call to emergency services. An outgoing call is
* identified as an emergency call by the dialer phone number.
*/
@VisibleForTesting
public boolean isEmergencyCall() {
return mIsEmergencyCall;
}
/**
* @return {@code true} if this an outgoing call to a test emergency number (and NOT to
* emergency services). Used for testing purposes to differentiate between a real and fake
* emergency call for safety reasons during testing.
*/
public boolean isTestEmergencyCall() {
return mIsTestEmergencyCall;
}
/**
* @return {@code true} if the network has identified this call as an emergency call.
*/
public boolean isNetworkIdentifiedEmergencyCall() {
return hasProperty(Connection.PROPERTY_NETWORK_IDENTIFIED_EMERGENCY_CALL);
}
/**
* @return The original handle this call is associated with. In-call services should use this
* handle when indicating in their UI the handle that is being called.
*/
public Uri getOriginalHandle() {
if (mGatewayInfo != null && !mGatewayInfo.isEmpty()) {
return mGatewayInfo.getOriginalAddress();
}
return getHandle();
}
@VisibleForTesting
public GatewayInfo getGatewayInfo() {
return mGatewayInfo;
}
void setGatewayInfo(GatewayInfo gatewayInfo) {
mGatewayInfo = gatewayInfo;
}
@VisibleForTesting
public PhoneAccountHandle getConnectionManagerPhoneAccount() {
return mConnectionManagerPhoneAccountHandle;
}
@VisibleForTesting
public void setConnectionManagerPhoneAccount(PhoneAccountHandle accountHandle) {
if (!Objects.equals(mConnectionManagerPhoneAccountHandle, accountHandle)) {
mConnectionManagerPhoneAccountHandle = accountHandle;
for (Listener l : mListeners) {
l.onConnectionManagerPhoneAccountChanged(this);
}
}
checkIfRttCapable();
}
/**
* @return the {@link PhoneAccountHandle} of the remote connection service which placing this
* call was delegated to, or {@code null} if a remote connection service was not used.
*/
public @Nullable PhoneAccountHandle getRemotePhoneAccountHandle() {
return mRemotePhoneAccountHandle;
}
/**
* Sets the {@link PhoneAccountHandle} of the remote connection service which placing this
* call was delegated to.
* @param accountHandle The phone account handle.
*/
public void setRemotePhoneAccountHandle(PhoneAccountHandle accountHandle) {
mRemotePhoneAccountHandle = accountHandle;
}
/**
* Determines which {@link PhoneAccountHandle} is actually placing a call.
* Where {@link #getRemotePhoneAccountHandle()} is non-null, the connection manager is placing
* the call via a remote connection service, so the remote connection service's phone account
* is the source.
* Where {@link #getConnectionManagerPhoneAccount()} is non-null and
* {@link #getRemotePhoneAccountHandle()} is null, the connection manager is placing the call
* itself (even if the target specifies something else).
* Finally, if neither of the above cases apply, the target phone account is the one actually
* placing the call.
* @return The {@link PhoneAccountHandle} which is actually placing a call.
*/
public @NonNull PhoneAccountHandle getDelegatePhoneAccountHandle() {
if (mRemotePhoneAccountHandle != null) {
return mRemotePhoneAccountHandle;
}
if (mConnectionManagerPhoneAccountHandle != null) {
return mConnectionManagerPhoneAccountHandle;
}
return mTargetPhoneAccountHandle;
}
@VisibleForTesting
public PhoneAccountHandle getTargetPhoneAccount() {
return mTargetPhoneAccountHandle;
}
@VisibleForTesting
public void setTargetPhoneAccount(PhoneAccountHandle accountHandle) {
if (!Objects.equals(mTargetPhoneAccountHandle, accountHandle)) {
mTargetPhoneAccountHandle = accountHandle;
for (Listener l : mListeners) {
l.onTargetPhoneAccountChanged(this);
}
configureCallAttributes();
}
checkIfVideoCapable();
checkIfRttCapable();
}
public CharSequence getTargetPhoneAccountLabel() {
if (getTargetPhoneAccount() == null) {
return null;
}
PhoneAccount phoneAccount = mCallsManager.getPhoneAccountRegistrar()
.getPhoneAccountUnchecked(getTargetPhoneAccount());
if (phoneAccount == null) {
return null;
}
return phoneAccount.getLabel();
}
/**
* Determines if this Call should be written to the call log.
* @return {@code true} for managed calls or for self-managed calls which have the
* {@link PhoneAccount#EXTRA_LOG_SELF_MANAGED_CALLS} extra set.
*/
public boolean isLoggedSelfManaged() {
if (!isSelfManaged()) {
// Managed calls are always logged.
return true;
}
if (getTargetPhoneAccount() == null) {
return false;
}
PhoneAccount phoneAccount = mCallsManager.getPhoneAccountRegistrar()
.getPhoneAccountUnchecked(getTargetPhoneAccount());
if (phoneAccount == null) {
return false;
}
if (getHandle() == null) {
// No point in logging a null-handle call. Some self-managed calls will have this.
return false;
}
if (!PhoneAccount.SCHEME_SIP.equals(getHandle().getScheme()) &&
!PhoneAccount.SCHEME_TEL.equals(getHandle().getScheme())) {
// Can't log schemes other than SIP or TEL for now.
return false;
}
return phoneAccount.getExtras() != null && phoneAccount.getExtras().getBoolean(
PhoneAccount.EXTRA_LOG_SELF_MANAGED_CALLS, false);
}
@VisibleForTesting
public boolean isIncoming() {
return mCallDirection == CALL_DIRECTION_INCOMING;
}
public boolean isExternalCall() {
return (getConnectionProperties() & Connection.PROPERTY_IS_EXTERNAL_CALL) ==
Connection.PROPERTY_IS_EXTERNAL_CALL;
}
public boolean isWorkCall() {
return mIsWorkCall;
}
public boolean isUsingCallRecordingTone() {
return mUseCallRecordingTone;
}
/**
* @return {@code true} if the {@link Call}'s {@link #getTargetPhoneAccount()} supports video.
*/
public boolean isVideoCallingSupportedByPhoneAccount() {
return mIsVideoCallingSupportedByPhoneAccount;
}
/**
* Sets whether video calling is supported by the current phone account. Since video support
* can change during a call, this method facilitates updating call video state.
* @param isVideoCallingSupported Sets whether video calling is supported.
*/
public void setVideoCallingSupportedByPhoneAccount(boolean isVideoCallingSupported) {
if (mIsVideoCallingSupportedByPhoneAccount == isVideoCallingSupported) {
return;
}
Log.i(this, "setVideoCallingSupportedByPhoneAccount: isSupp=%b", isVideoCallingSupported);
mIsVideoCallingSupportedByPhoneAccount = isVideoCallingSupported;
// Force an update of the connection capabilities so that the dialer is informed of the new
// video capabilities based on the phone account's support for video.
setConnectionCapabilities(getConnectionCapabilities(), true /* force */);
}
/**
* Determines if pulling this external call is supported. If it is supported, we will allow the
* {@link Connection#CAPABILITY_CAN_PULL_CALL} capability to be added to this call's
* capabilities. If it is not supported, we will strip this capability before sending this
* call's capabilities to the InCallService.
* @param isPullExternalCallSupported true, if pulling this external call is supported, false
* otherwise.
*/
public void setIsPullExternalCallSupported(boolean isPullExternalCallSupported) {
if (!isExternalCall()) return;
if (isPullExternalCallSupported == mIsPullExternalCallSupported) return;
Log.i(this, "setCanPullExternalCall: canPull=%b", isPullExternalCallSupported);
mIsPullExternalCallSupported = isPullExternalCallSupported;
// Use mConnectionCapabilities here to get the unstripped capabilities.
setConnectionCapabilities(mConnectionCapabilities, true /* force */);
}
/**
* @return {@code true} if the {@link Call} locally supports video.
*/
public boolean isLocallyVideoCapable() {
return (getConnectionCapabilities() & Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL)
== Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL;
}
public boolean isSelfManaged() {
return mIsSelfManaged;
}
public void setIsSelfManaged(boolean isSelfManaged) {
mIsSelfManaged = isSelfManaged;
// Connection properties will add/remove the PROPERTY_SELF_MANAGED.
setConnectionProperties(getConnectionProperties());
}
public boolean visibleToInCallService() {
return mVisibleToInCallService;
}
public void setVisibleToInCallService(boolean visibleToInCallService) {
mVisibleToInCallService = visibleToInCallService;
}
public void markFinishedHandoverStateAndCleanup(int handoverState) {
if (mHandoverSourceCall != null) {
mHandoverSourceCall.setHandoverState(handoverState);
} else if (mHandoverDestinationCall != null) {
mHandoverDestinationCall.setHandoverState(handoverState);
}
setHandoverState(handoverState);
maybeCleanupHandover();
}
public void maybeCleanupHandover() {
if (mHandoverSourceCall != null) {
mHandoverSourceCall.setHandoverSourceCall(null);
mHandoverSourceCall.setHandoverDestinationCall(null);
mHandoverSourceCall = null;
} else if (mHandoverDestinationCall != null) {
mHandoverDestinationCall.setHandoverSourceCall(null);
mHandoverDestinationCall.setHandoverDestinationCall(null);
mHandoverDestinationCall = null;
}
}
public boolean isHandoverInProgress() {
return mHandoverSourceCall != null || mHandoverDestinationCall != null;
}
public Call getHandoverDestinationCall() {
return mHandoverDestinationCall;
}
public void setHandoverDestinationCall(Call call) {
mHandoverDestinationCall = call;
}
public Call getHandoverSourceCall() {
return mHandoverSourceCall;
}
public void setHandoverSourceCall(Call call) {
mHandoverSourceCall = call;
}
public void setHandoverState(int handoverState) {
Log.d(this, "setHandoverState: callId=%s, handoverState=%s", getId(),
HandoverState.stateToString(handoverState));
mHandoverState = handoverState;
}
public int getHandoverState() {
return mHandoverState;
}
private void configureCallAttributes() {
PhoneAccountRegistrar phoneAccountRegistrar = mCallsManager.getPhoneAccountRegistrar();
boolean isWorkCall = false;
boolean isCallRecordingToneSupported = false;
boolean isSimCall = false;
PhoneAccount phoneAccount =
phoneAccountRegistrar.getPhoneAccountUnchecked(mTargetPhoneAccountHandle);
if (phoneAccount != null) {
final UserHandle userHandle;
if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) {
userHandle = mInitiatingUser;
} else {
userHandle = mTargetPhoneAccountHandle.getUserHandle();
}
if (userHandle != null) {
isWorkCall = UserUtil.isManagedProfile(mContext, userHandle);
}
isCallRecordingToneSupported = (phoneAccount.hasCapabilities(
PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) && phoneAccount.getExtras() != null
&& phoneAccount.getExtras().getBoolean(
PhoneAccount.EXTRA_PLAY_CALL_RECORDING_TONE, false));
isSimCall = phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION);
}
mIsWorkCall = isWorkCall;
mUseCallRecordingTone = isCallRecordingToneSupported;
mIsSimCall = isSimCall;
}
/**
* Caches the state of the {@link PhoneAccount#CAPABILITY_VIDEO_CALLING} {@link PhoneAccount}
* capability and ensures that the video state is updated if the phone account does not support
* video calling.
*/
private void checkIfVideoCapable() {
PhoneAccountRegistrar phoneAccountRegistrar = mCallsManager.getPhoneAccountRegistrar();
if (mTargetPhoneAccountHandle == null) {
// If no target phone account handle is specified, assume we can potentially perform a
// video call; once the phone account is set, we can confirm that it is video capable.
mIsVideoCallingSupportedByPhoneAccount = true;
Log.d(this, "checkIfVideoCapable: no phone account selected; assume video capable.");
return;
}
PhoneAccount phoneAccount =
phoneAccountRegistrar.getPhoneAccountUnchecked(mTargetPhoneAccountHandle);
mIsVideoCallingSupportedByPhoneAccount = phoneAccount != null &&
phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING);
if (!mIsVideoCallingSupportedByPhoneAccount && VideoProfile.isVideo(getVideoState())) {
// The PhoneAccount for the Call was set to one which does not support video calling,
// and the current call is configured to be a video call; downgrade to audio-only.
setVideoState(VideoProfile.STATE_AUDIO_ONLY);
Log.d(this, "checkIfVideoCapable: selected phone account doesn't support video.");
}
}
private void checkIfRttCapable() {
PhoneAccountRegistrar phoneAccountRegistrar = mCallsManager.getPhoneAccountRegistrar();
if (mTargetPhoneAccountHandle == null) {
return;
}
// Check both the target phone account and the connection manager phone account -- if
// either support RTT, just set the streams and have them set/unset the RTT property as
// needed.
PhoneAccount phoneAccount =
phoneAccountRegistrar.getPhoneAccountUnchecked(mTargetPhoneAccountHandle);
PhoneAccount connectionManagerPhoneAccount = phoneAccountRegistrar.getPhoneAccountUnchecked(
mConnectionManagerPhoneAccountHandle);
boolean isRttSupported = phoneAccount != null && phoneAccount.hasCapabilities(
PhoneAccount.CAPABILITY_RTT);
boolean isConnectionManagerRttSupported = connectionManagerPhoneAccount != null
&& connectionManagerPhoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_RTT);
if ((isConnectionManagerRttSupported || isRttSupported)
&& mDidRequestToStartWithRtt && !areRttStreamsInitialized()) {
// If the phone account got set to an RTT capable one and we haven't set the streams
// yet, do so now.
createRttStreams();
Log.i(this, "Setting RTT streams after target phone account selected");
}
}
boolean shouldAttachToExistingConnection() {
return mShouldAttachToExistingConnection;
}
/**
* Note: This method relies on {@link #mConnectElapsedTimeMillis} and
* {@link #mDisconnectElapsedTimeMillis} which are independent of the wall clock (which could
* change due to clock changes).
* @return The "age" of this call object in milliseconds, which typically also represents the
* period since this call was added to the set pending outgoing calls.
*/
@VisibleForTesting
public long getAgeMillis() {
if (mState == CallState.DISCONNECTED &&
(mDisconnectCause.getCode() == DisconnectCause.REJECTED ||
mDisconnectCause.getCode() == DisconnectCause.MISSED)) {
// Rejected and missed calls have no age. They're immortal!!
return 0;
} else if (mConnectElapsedTimeMillis == 0) {
// Age is measured in the amount of time the call was active. A zero connect time
// indicates that we never went active, so return 0 for the age.
return 0;
} else if (mDisconnectElapsedTimeMillis == 0) {
// We connected, but have not yet disconnected
return mClockProxy.elapsedRealtime() - mConnectElapsedTimeMillis;
}
return mDisconnectElapsedTimeMillis - mConnectElapsedTimeMillis;
}
/**
* @return The time when this call object was created and added to the set of pending outgoing
* calls.
*/
public long getCreationTimeMillis() {
return mCreationTimeMillis;
}
public void setCreationTimeMillis(long time) {
mCreationTimeMillis = time;
}
public long getConnectTimeMillis() {
return mConnectTimeMillis;
}
public void setConnectTimeMillis(long connectTimeMillis) {
mConnectTimeMillis = connectTimeMillis;
}
public void setConnectElapsedTimeMillis(long connectElapsedTimeMillis) {
mConnectElapsedTimeMillis = connectElapsedTimeMillis;
}
public int getConnectionCapabilities() {
return stripUnsupportedCapabilities(mConnectionCapabilities);
}
int getConnectionProperties() {
return mConnectionProperties;
}
public void setConnectionCapabilities(int connectionCapabilities) {
setConnectionCapabilities(connectionCapabilities, false /* forceUpdate */);
}
void setConnectionCapabilities(int connectionCapabilities, boolean forceUpdate) {
Log.v(this, "setConnectionCapabilities: %s", Connection.capabilitiesToString(
connectionCapabilities));
if (forceUpdate || mConnectionCapabilities != connectionCapabilities) {
int previousCapabilities = mConnectionCapabilities;
mConnectionCapabilities = connectionCapabilities;
for (Listener l : mListeners) {
l.onConnectionCapabilitiesChanged(this);
}
int strippedCaps = getConnectionCapabilities();
int xorCaps = previousCapabilities ^ strippedCaps;
Log.addEvent(this, LogUtils.Events.CAPABILITY_CHANGE,
"Current: [%s], Removed [%s], Added [%s]",
Connection.capabilitiesToStringShort(strippedCaps),
Connection.capabilitiesToStringShort(previousCapabilities & xorCaps),
Connection.capabilitiesToStringShort(strippedCaps & xorCaps));
}
}
/**
* For some states of Telecom, we need to modify this connection's capabilities:
* - A user should not be able to pull an external call during an emergency call, so
* CAPABILITY_CAN_PULL_CALL should be removed until the emergency call ends.
* @param capabilities The original capabilities.
* @return The stripped capabilities.
*/
private int stripUnsupportedCapabilities(int capabilities) {
if (!mIsPullExternalCallSupported) {
if ((capabilities |= Connection.CAPABILITY_CAN_PULL_CALL) > 0) {
capabilities &= ~Connection.CAPABILITY_CAN_PULL_CALL;
Log.i(this, "stripCapabilitiesBasedOnState: CAPABILITY_CAN_PULL_CALL removed.");
}
}
return capabilities;
}
public void setConnectionProperties(int connectionProperties) {
Log.v(this, "setConnectionProperties: %s", Connection.propertiesToString(
connectionProperties));
// Ensure the ConnectionService can't change the state of the self-managed property.
if (isSelfManaged()) {
connectionProperties |= Connection.PROPERTY_SELF_MANAGED;
} else {
connectionProperties &= ~Connection.PROPERTY_SELF_MANAGED;
}
int changedProperties = mConnectionProperties ^ connectionProperties;
if (changedProperties != 0) {
int previousProperties = mConnectionProperties;
mConnectionProperties = connectionProperties;
boolean didRttChange =
(changedProperties & Connection.PROPERTY_IS_RTT) == Connection.PROPERTY_IS_RTT;
if (didRttChange) {
if ((mConnectionProperties & Connection.PROPERTY_IS_RTT) ==
Connection.PROPERTY_IS_RTT) {
// If we already had RTT streams up, that means that either the call started
// with RTT or the user previously requested to start RTT. Either way, don't
// play the alert tone.
if (!areRttStreamsInitialized()) {
mCallsManager.playRttUpgradeToneForCall(this);
}
createRttStreams();
// Call startRtt to pass the RTT pipes down to the connection service.
// They already turned on the RTT property so no request should be sent.
mConnectionService.startRtt(this,
getInCallToCsRttPipeForCs(), getCsToInCallRttPipeForCs());
mWasEverRtt = true;
if (isEmergencyCall()) {
mCallsManager.mute(false);
}
} else {
closeRttStreams();
mInCallToConnectionServiceStreams = null;
mConnectionServiceToInCallStreams = null;
}
}
mWasHighDefAudio = (connectionProperties & Connection.PROPERTY_HIGH_DEF_AUDIO) ==
Connection.PROPERTY_HIGH_DEF_AUDIO;
mWasWifi = (connectionProperties & Connection.PROPERTY_WIFI) > 0;
for (Listener l : mListeners) {
l.onConnectionPropertiesChanged(this, didRttChange);
}
boolean wasExternal = (previousProperties & Connection.PROPERTY_IS_EXTERNAL_CALL)
== Connection.PROPERTY_IS_EXTERNAL_CALL;
boolean isExternal = (connectionProperties & Connection.PROPERTY_IS_EXTERNAL_CALL)
== Connection.PROPERTY_IS_EXTERNAL_CALL;
if (wasExternal != isExternal) {
Log.v(this, "setConnectionProperties: external call changed isExternal = %b",
isExternal);
Log.addEvent(this, LogUtils.Events.IS_EXTERNAL, isExternal);
if (isExternal) {
// If there is an ongoing emergency call, remove the ability for this call to
// be pulled.
boolean isInEmergencyCall = mCallsManager.isInEmergencyCall();
setIsPullExternalCallSupported(!isInEmergencyCall);
}
for (Listener l : mListeners) {
l.onExternalCallChanged(this, isExternal);
}
}
boolean wasDowngradedConference =
(previousProperties & Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE) != 0;
boolean isDowngradedConference =
(connectionProperties & Connection.PROPERTY_IS_DOWNGRADED_CONFERENCE) != 0;
if (wasDowngradedConference && !isDowngradedConference) {
Log.i(this, "DOWNGRADED_CONFERENCE property removed; setting"
+ " conference state to false");
setConferenceState(false);
}
mAnalytics.addCallProperties(mConnectionProperties);
int xorProps = previousProperties ^ mConnectionProperties;
Log.addEvent(this, LogUtils.Events.PROPERTY_CHANGE,
"Current: [%s], Removed [%s], Added [%s]",
Connection.propertiesToStringShort(mConnectionProperties),
Connection.propertiesToStringShort(previousProperties & xorProps),
Connection.propertiesToStringShort(mConnectionProperties & xorProps));
}
}
public int getSupportedAudioRoutes() {
return mSupportedAudioRoutes;
}
void setSupportedAudioRoutes(int audioRoutes) {
if (mSupportedAudioRoutes != audioRoutes) {
mSupportedAudioRoutes = audioRoutes;
}
}
@VisibleForTesting
public Call getParentCall() {
return mParentCall;
}
@VisibleForTesting
public List<Call> getChildCalls() {
return mChildCalls;
}
@VisibleForTesting
public boolean wasConferencePreviouslyMerged() {
return mWasConferencePreviouslyMerged;
}
public boolean isDisconnectingChildCall() {
return mIsDisconnectingChildCall;
}
/**
* Sets whether this call is a child call.
*/
private void maybeSetCallAsDisconnectingChild() {
if (mParentCall != null) {
mIsDisconnectingChildCall = true;
}
}
@VisibleForTesting
public Call getConferenceLevelActiveCall() {
return mConferenceLevelActiveCall;
}
@VisibleForTesting
public ConnectionServiceWrapper getConnectionService() {
return mConnectionService;
}
/**
* Retrieves the {@link Context} for the call.
*
* @return The {@link Context}.
*/
public Context getContext() {
return mContext;
}
@VisibleForTesting
public void setConnectionService(ConnectionServiceWrapper service) {
Preconditions.checkNotNull(service);
clearConnectionService();
service.incrementAssociatedCallCount();
mConnectionService = service;
mAnalytics.setCallConnectionService(service.getComponentName().flattenToShortString());
mConnectionService.addCall(this);
}
/**
* Perform an in-place replacement of the {@link ConnectionServiceWrapper} for this Call.
* Removes the call from its former {@link ConnectionServiceWrapper}, ensuring that the
* ConnectionService is NOT unbound if the call count hits zero.
* This is used by the {@link ConnectionServiceWrapper} when handling {@link Connection} and
* {@link Conference} additions via a ConnectionManager.
* The original {@link android.telecom.ConnectionService} will directly add external calls and
* conferences to Telecom as well as the ConnectionManager, which will add to Telecom. In these
* cases since its first added to via the original CS, we want to change the CS responsible for
* the call to the ConnectionManager rather than adding it again as another call/conference.
*
* @param service The new {@link ConnectionServiceWrapper}.
*/
public void replaceConnectionService(ConnectionServiceWrapper service) {
Preconditions.checkNotNull(service);
if (mConnectionService != null) {
ConnectionServiceWrapper serviceTemp = mConnectionService;
mConnectionService = null;
serviceTemp.removeCall(this);
serviceTemp.decrementAssociatedCallCount(true /*isSuppressingUnbind*/);
}
service.incrementAssociatedCallCount();
mConnectionService = service;
mAnalytics.setCallConnectionService(service.getComponentName().flattenToShortString());
}
/**
* Clears the associated connection service.
*/
void clearConnectionService() {
if (mConnectionService != null) {
ConnectionServiceWrapper serviceTemp = mConnectionService;
mConnectionService = null;
serviceTemp.removeCall(this);
// Decrementing the count can cause the service to unbind, which itself can trigger the
// service-death code. Since the service death code tries to clean up any associated
// calls, we need to make sure to remove that information (e.g., removeCall()) before
// we decrement. Technically, invoking removeCall() prior to decrementing is all that is
// necessary, but cleaning up mConnectionService prior to triggering an unbind is good
// to do.
decrementAssociatedCallCount(serviceTemp);
}
}
/**
* Starts the create connection sequence. Upon completion, there should exist an active
* connection through a connection service (or the call will have failed).
*
* @param phoneAccountRegistrar The phone account registrar.
*/
void startCreateConnection(PhoneAccountRegistrar phoneAccountRegistrar) {
if (mCreateConnectionProcessor != null) {
Log.w(this, "mCreateConnectionProcessor in startCreateConnection is not null. This is" +
" due to a race between NewOutgoingCallIntentBroadcaster and " +
"phoneAccountSelected, but is harmlessly resolved by ignoring the second " +
"invocation.");
return;
}
mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this,
phoneAccountRegistrar, mContext);
mCreateConnectionProcessor.process();
}
@Override
public void handleCreateConferenceSuccess(
CallIdMapper idMapper,
ParcelableConference conference) {
Log.v(this, "handleCreateConferenceSuccessful %s", conference);
setTargetPhoneAccount(conference.getPhoneAccount());
setHandle(conference.getHandle(), conference.getHandlePresentation());
setConnectionCapabilities(conference.getConnectionCapabilities());
setConnectionProperties(conference.getConnectionProperties());
setVideoProvider(conference.getVideoProvider());
setVideoState(conference.getVideoState());
setRingbackRequested(conference.isRingbackRequested());
setStatusHints(conference.getStatusHints());
putExtras(SOURCE_CONNECTION_SERVICE, conference.getExtras());
switch (mCallDirection) {
case CALL_DIRECTION_INCOMING:
// Listeners (just CallsManager for now) will be responsible for checking whether
// the call should be blocked.
for (Listener l : mListeners) {
l.onSuccessfulIncomingCall(this);
}
break;
case CALL_DIRECTION_OUTGOING:
for (Listener l : mListeners) {
l.onSuccessfulOutgoingCall(this,
getStateFromConnectionState(conference.getState()));
}
break;
}
}
@Override
public void handleCreateConnectionSuccess(
CallIdMapper idMapper,
ParcelableConnection connection) {
Log.v(this, "handleCreateConnectionSuccessful %s", connection);
setTargetPhoneAccount(connection.getPhoneAccount());
setHandle(connection.getHandle(), connection.getHandlePresentation());
setCallerDisplayName(
connection.getCallerDisplayName(), connection.getCallerDisplayNamePresentation());
setConnectionCapabilities(connection.getConnectionCapabilities());
setConnectionProperties(connection.getConnectionProperties());
setIsVoipAudioMode(connection.getIsVoipAudioMode());
setSupportedAudioRoutes(connection.getSupportedAudioRoutes());
setVideoProvider(connection.getVideoProvider());
setVideoState(connection.getVideoState());
setRingbackRequested(connection.isRingbackRequested());
setStatusHints(connection.getStatusHints());
putExtras(SOURCE_CONNECTION_SERVICE, connection.getExtras());
mConferenceableCalls.clear();
for (String id : connection.getConferenceableConnectionIds()) {
mConferenceableCalls.add(idMapper.getCall(id));
}
switch (mCallDirection) {
case CALL_DIRECTION_INCOMING:
setCallerNumberVerificationStatus(connection.getCallerNumberVerificationStatus());
// Listeners (just CallsManager for now) will be responsible for checking whether
// the call should be blocked.
for (Listener l : mListeners) {
l.onSuccessfulIncomingCall(this);
}
break;
case CALL_DIRECTION_OUTGOING:
for (Listener l : mListeners) {
l.onSuccessfulOutgoingCall(this,
getStateFromConnectionState(connection.getState()));
}
break;
case CALL_DIRECTION_UNKNOWN:
for (Listener l : mListeners) {
l.onSuccessfulUnknownCall(this, getStateFromConnectionState(connection
.getState()));
}
break;
}
}
@Override
public void handleCreateConferenceFailure(DisconnectCause disconnectCause) {
clearConnectionService();
setDisconnectCause(disconnectCause);
mCallsManager.markCallAsDisconnected(this, disconnectCause);
switch (mCallDirection) {
case CALL_DIRECTION_INCOMING:
for (Listener listener : mListeners) {
listener.onFailedIncomingCall(this);
}
break;
case CALL_DIRECTION_OUTGOING:
for (Listener listener : mListeners) {
listener.onFailedOutgoingCall(this, disconnectCause);
}
break;
}
}
@Override
public void handleCreateConnectionFailure(DisconnectCause disconnectCause) {
clearConnectionService();
setDisconnectCause(disconnectCause);
mCallsManager.markCallAsDisconnected(this, disconnectCause);
switch (mCallDirection) {
case CALL_DIRECTION_INCOMING:
for (Listener listener : mListeners) {
listener.onFailedIncomingCall(this);
}
break;
case CALL_DIRECTION_OUTGOING:
for (Listener listener : mListeners) {
listener.onFailedOutgoingCall(this, disconnectCause);
}
break;
case CALL_DIRECTION_UNKNOWN:
for (Listener listener : mListeners) {
listener.onFailedUnknownCall(this);
}
break;
}
}
/**
* Plays the specified DTMF tone.
*/
@VisibleForTesting
public void playDtmfTone(char digit) {
if (mConnectionService == null) {
Log.w(this, "playDtmfTone() request on a call without a connection service.");
} else {
Log.i(this, "Send playDtmfTone to connection service for call %s", this);
mConnectionService.playDtmfTone(this, digit);
Log.addEvent(this, LogUtils.Events.START_DTMF, Log.pii(digit));
}
mPlayingDtmfTone = digit;
}
/**
* Stops playing any currently playing DTMF tone.
*/
@VisibleForTesting
public void stopDtmfTone() {
if (mConnectionService == null) {
Log.w(this, "stopDtmfTone() request on a call without a connection service.");
} else {
Log.i(this, "Send stopDtmfTone to connection service for call %s", this);
Log.addEvent(this, LogUtils.Events.STOP_DTMF);
mConnectionService.stopDtmfTone(this);
}
mPlayingDtmfTone = NO_DTMF_TONE;
}
/**
* @return {@code true} if a DTMF tone has been started via {@link #playDtmfTone(char)} but has
* not been stopped via {@link #stopDtmfTone()}, {@code false} otherwise.
*/
boolean isDtmfTonePlaying() {
return mPlayingDtmfTone != NO_DTMF_TONE;
}
/**
* Silences the ringer.
*/
void silence() {
if (mConnectionService == null) {
Log.w(this, "silence() request on a call without a connection service.");
} else {
Log.i(this, "Send silence to connection service for call %s", this);
Log.addEvent(this, LogUtils.Events.SILENCE);
mConnectionService.silence(this);
}
}
@VisibleForTesting
public void disconnect() {
disconnect(0);
}
@VisibleForTesting
public void disconnect(String reason) {
disconnect(0, reason);
}
/**
* Attempts to disconnect the call through the connection service.
*/
@VisibleForTesting
public void disconnect(long disconnectionTimeout) {
disconnect(disconnectionTimeout, "internal" /** reason */);
}
/**
* Attempts to disconnect the call through the connection service.
* @param reason the reason for the disconnect; used for logging purposes only. In some cases
* this can be a package name if the disconnect was initiated through an API such
* as TelecomManager.
*/
@VisibleForTesting
public void disconnect(long disconnectionTimeout, String reason) {
Log.addEvent(this, LogUtils.Events.REQUEST_DISCONNECT, reason);
// Track that the call is now locally disconnecting.
setLocallyDisconnecting(true);
maybeSetCallAsDisconnectingChild();
if (mState == CallState.NEW || mState == CallState.SELECT_PHONE_ACCOUNT ||
mState == CallState.CONNECTING) {
Log.v(this, "Aborting call %s", this);
abort(disconnectionTimeout);
} else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
if (mState == CallState.AUDIO_PROCESSING && !hasGoneActiveBefore()) {
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
} else if (mState == CallState.SIMULATED_RINGING) {
// This is the case where the dialer calls disconnect() because the call timed out
// or an emergency call was dialed while in this state.
// Override the disconnect cause to MISSED
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED));
}
if (mConnectionService == null) {
Log.e(this, new Exception(), "disconnect() request on a call without a"
+ " connection service.");
} else {
Log.i(this, "Send disconnect to connection service for call: %s", this);
// The call isn't officially disconnected until the connection service
// confirms that the call was actually disconnected. Only then is the
// association between call and connection service severed, see
// {@link CallsManager#markCallAsDisconnected}.
mConnectionService.disconnect(this);
}
}
}
void abort(long disconnectionTimeout) {
if (mCreateConnectionProcessor != null &&
!mCreateConnectionProcessor.isProcessingComplete()) {
mCreateConnectionProcessor.abort();
} else if (mState == CallState.NEW || mState == CallState.SELECT_PHONE_ACCOUNT
|| mState == CallState.CONNECTING) {
if (disconnectionTimeout > 0) {
// If the cancelation was from NEW_OUTGOING_CALL with a timeout of > 0
// milliseconds, do not destroy the call.
// Instead, we announce the cancellation and CallsManager handles
// it through a timer. Since apps often cancel calls through NEW_OUTGOING_CALL and
// then re-dial them quickly using a gateway, allowing the first call to end
// causes jank. This timeout allows CallsManager to transition the first call into
// the second call so that in-call only ever sees a single call...eliminating the
// jank altogether. The app will also be able to set the timeout via an extra on
// the ordered broadcast.
for (Listener listener : mListeners) {
if (listener.onCanceledViaNewOutgoingCallBroadcast(
this, disconnectionTimeout)) {
// The first listener to handle this wins. A return value of true means that
// the listener will handle the disconnection process later and so we
// should not continue it here.
setLocallyDisconnecting(false);
return;
}
}
}
handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.CANCELED));
} else {
Log.v(this, "Cannot abort a call which is neither SELECT_PHONE_ACCOUNT or CONNECTING");
}
}
/**
* Answers the call if it is ringing.
*
* @param videoState The video state in which to answer the call.
*/
@VisibleForTesting
public void answer(int videoState) {
// Check to verify that the call is still in the ringing state. A call can change states
// between the time the user hits 'answer' and Telecom receives the command.
if (isRinging("answer")) {
if (!isVideoCallingSupportedByPhoneAccount() && VideoProfile.isVideo(videoState)) {
// Video calling is not supported, yet the InCallService is attempting to answer as
// video. We will simply answer as audio-only.
videoState = VideoProfile.STATE_AUDIO_ONLY;
}
// At this point, we are asking the connection service to answer but we don't assume
// that it will work. Instead, we wait until confirmation from the connectino service
// that the call is in a non-STATE_RINGING state before changing the UI. See
// {@link ConnectionServiceAdapter#setActive} and other set* methods.
if (mConnectionService != null) {
mConnectionService.answer(this, videoState);
} else {
Log.e(this, new NullPointerException(),
"answer call failed due to null CS callId=%s", getId());
}
Log.addEvent(this, LogUtils.Events.REQUEST_ACCEPT);
}
}
/**
* Answers the call on the connectionservice side in order to start audio processing.
*
* This pathway keeps the call in the ANSWERED state until the connection service confirms the
* answer, at which point we'll set it to AUDIO_PROCESSING. However, to prevent any other
* components from seeing the churn between RINGING -> ANSWERED -> AUDIO_PROCESSING, we'll
* refrain from tracking this call in CallsManager until we've stabilized in AUDIO_PROCESSING
*/
public void answerForAudioProcessing() {
if (mState != CallState.RINGING) {
Log.w(this, "Trying to audio-process a non-ringing call: id=%s", mId);
return;
}
if (mConnectionService != null) {
mConnectionService.answer(this, VideoProfile.STATE_AUDIO_ONLY);
} else {
Log.e(this, new NullPointerException(),
"answer call (audio processing) failed due to null CS callId=%s", getId());
}
Log.addEvent(this, LogUtils.Events.REQUEST_PICKUP_FOR_AUDIO_PROCESSING);
}
public void setAudioProcessingRequestingApp(CharSequence appName) {
mAudioProcessingRequestingApp = appName;
}
public CharSequence getAudioProcessingRequestingApp() {
return mAudioProcessingRequestingApp;
}
/**
* Deflects the call if it is ringing.
*
* @param address address to be deflected to.
*/
@VisibleForTesting
public void deflect(Uri address) {
// Check to verify that the call is still in the ringing state. A call can change states
// between the time the user hits 'deflect' and Telecomm receives the command.
if (isRinging("deflect")) {
// At this point, we are asking the connection service to deflect but we don't assume
// that it will work. Instead, we wait until confirmation from the connection service
// that the call is in a non-STATE_RINGING state before changing the UI. See
// {@link ConnectionServiceAdapter#setActive} and other set* methods.
mVideoStateHistory |= mVideoState;
if (mConnectionService != null) {
mConnectionService.deflect(this, address);
} else {
Log.e(this, new NullPointerException(),
"deflect call failed due to null CS callId=%s", getId());
}
Log.addEvent(this, LogUtils.Events.REQUEST_DEFLECT, Log.pii(address));
}
}
/**
* Rejects the call if it is ringing.
*
* @param rejectWithMessage Whether to send a text message as part of the call rejection.
* @param textMessage An optional text message to send as part of the rejection.
*/
@VisibleForTesting
public void reject(boolean rejectWithMessage, String textMessage) {
reject(rejectWithMessage, textMessage, "internal" /** reason */);
}
/**
* Rejects the call if it is ringing.
*
* @param rejectWithMessage Whether to send a text message as part of the call rejection.
* @param textMessage An optional text message to send as part of the rejection.
* @param reason The reason for the reject; used for logging purposes. May be a package name
* if the reject is initiated from an API such as TelecomManager.
*/
@VisibleForTesting
public void reject(boolean rejectWithMessage, String textMessage, String reason) {
if (mState == CallState.SIMULATED_RINGING) {
// This handles the case where the user manually rejects a call that's in simulated
// ringing. Since the call is already active on the connectionservice side, we want to
// hangup, not reject.
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
if (mConnectionService != null) {
mConnectionService.disconnect(this);
} else {
Log.e(this, new NullPointerException(),
"reject call failed due to null CS callId=%s", getId());
}
Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason);
} else if (isRinging("reject") || isAnswered("reject")) {
// Ensure video state history tracks video state at time of rejection.
mVideoStateHistory |= mVideoState;
if (mConnectionService != null) {
mConnectionService.reject(this, rejectWithMessage, textMessage);
} else {
Log.e(this, new NullPointerException(),
"reject call failed due to null CS callId=%s", getId());
}
Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, reason);
}
}
/**
* Reject this Telecom call with the user-indicated reason.
* @param rejectReason The user-indicated reason fore rejecting the call.
*/
public void reject(@android.telecom.Call.RejectReason int rejectReason) {
if (mState == CallState.SIMULATED_RINGING) {
// This handles the case where the user manually rejects a call that's in simulated
// ringing. Since the call is already active on the connectionservice side, we want to
// hangup, not reject.
// Since its simulated reason we can't pass along the reject reason.
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
if (mConnectionService != null) {
mConnectionService.disconnect(this);
} else {
Log.e(this, new NullPointerException(),
"reject call failed due to null CS callId=%s", getId());
}
Log.addEvent(this, LogUtils.Events.REQUEST_REJECT);
} else if (isRinging("reject") || isAnswered("reject")) {
// Ensure video state history tracks video state at time of rejection.
mVideoStateHistory |= mVideoState;
if (mConnectionService != null) {
mConnectionService.rejectWithReason(this, rejectReason);
} else {
Log.e(this, new NullPointerException(),
"reject call failed due to null CS callId=%s", getId());
}
Log.addEvent(this, LogUtils.Events.REQUEST_REJECT, rejectReason);
}
}
/**
* Transfers the call if it is active or held.
*
* @param number number to be transferred to.
* @param isConfirmationRequired whether for blind or assured transfer.
*/
@VisibleForTesting
public void transfer(Uri number, boolean isConfirmationRequired) {
if (mState == CallState.ACTIVE || mState == CallState.ON_HOLD) {
if (mConnectionService != null) {
mConnectionService.transfer(this, number, isConfirmationRequired);
} else {
Log.e(this, new NullPointerException(),
"transfer call failed due to null CS callId=%s", getId());
}
Log.addEvent(this, LogUtils.Events.REQUEST_TRANSFER, Log.pii(number));
}
}
/**
* Transfers the call when this call is active and the other call is held.
* This is for Consultative call transfer.
*
* @param otherCall The other {@link Call} to which this call will be transferred.
*/
@VisibleForTesting
public void transfer(Call otherCall) {
if (mState == CallState.ACTIVE &&
(otherCall != null && otherCall.getState() == CallState.ON_HOLD)) {
if (mConnectionService != null) {
mConnectionService.transfer(this, otherCall);
} else {
Log.e(this, new NullPointerException(),
"transfer call failed due to null CS callId=%s", getId());
}
Log.addEvent(this, LogUtils.Events.REQUEST_CONSULTATIVE_TRANSFER, otherCall);
}
}
/**
* Puts the call on hold if it is currently active.
*/
@VisibleForTesting
public void hold() {
hold(null /* reason */);
}
public void hold(String reason) {
if (mState == CallState.ACTIVE) {
if (mConnectionService != null) {
mConnectionService.hold(this);
} else {
Log.e(this, new NullPointerException(),
"hold call failed due to null CS callId=%s", getId());
}
Log.addEvent(this, LogUtils.Events.REQUEST_HOLD, reason);
}
}
/**
* Releases the call from hold if it is currently active.
*/
@VisibleForTesting
public void unhold() {
unhold(null /* reason */);
}
public void unhold(String reason) {
if (mState == CallState.ON_HOLD) {
if (mConnectionService != null) {
mConnectionService.unhold(this);
} else {
Log.e(this, new NullPointerException(),
"unhold call failed due to null CS callId=%s", getId());
}
Log.addEvent(this, LogUtils.Events.REQUEST_UNHOLD, reason);
}
}
/** Checks if this is a live call or not. */
@VisibleForTesting
public boolean isAlive() {
switch (mState) {
case CallState.NEW:
case CallState.RINGING:
case CallState.ANSWERED:
case CallState.DISCONNECTED:
case CallState.ABORTED:
return false;
default:
return true;
}
}
@VisibleForTesting
public boolean isActive() {
return mState == CallState.ACTIVE;
}
@VisibleForTesting
public Bundle getExtras() {
return mExtras;
}
/**
* Adds extras to the extras bundle associated with this {@link Call}.
*
* Note: this method needs to know the source of the extras change (see
* {@link #SOURCE_CONNECTION_SERVICE}, {@link #SOURCE_INCALL_SERVICE}). Extras changes which
* originate from a connection service will only be notified to incall services. Likewise,
* changes originating from the incall services will only notify the connection service of the
* change.
*
* @param source The source of the extras addition.
* @param extras The extras.
*/
public void putExtras(int source, Bundle extras) {
if (extras == null) {
return;
}
if (mExtras == null) {
mExtras = new Bundle();
}
mExtras.putAll(extras);
for (Listener l : mListeners) {
l.onExtrasChanged(this, source, extras);
}
// If mExtra shows that the call using Volte, record it with mWasVolte
if (mExtras.containsKey(TelecomManager.EXTRA_CALL_NETWORK_TYPE) &&
mExtras.get(TelecomManager.EXTRA_CALL_NETWORK_TYPE)
.equals(TelephonyManager.NETWORK_TYPE_LTE)) {
mWasVolte = true;
}
if (extras.containsKey(Connection.EXTRA_ORIGINAL_CONNECTION_ID)) {
setOriginalConnectionId(extras.getString(Connection.EXTRA_ORIGINAL_CONNECTION_ID));
}
if (extras.containsKey(Connection.EXTRA_CALLER_NUMBER_VERIFICATION_STATUS)
&& source == SOURCE_CONNECTION_SERVICE) {
int callerNumberVerificationStatus =
extras.getInt(Connection.EXTRA_CALLER_NUMBER_VERIFICATION_STATUS);
if (mCallerNumberVerificationStatus != callerNumberVerificationStatus) {
Log.addEvent(this, LogUtils.Events.VERSTAT_CHANGED, callerNumberVerificationStatus);
setCallerNumberVerificationStatus(callerNumberVerificationStatus);
}
}
// The remote connection service API can track the phone account which was originally
// requested to create a connection via the remote connection service API; we store that so
// we have some visibility into how a call was actually placed.
if (mExtras.containsKey(Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE)) {
setRemotePhoneAccountHandle(extras.getParcelable(
Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE));
}
// If the change originated from an InCallService, notify the connection service.
if (source == SOURCE_INCALL_SERVICE) {
if (mConnectionService != null) {
mConnectionService.onExtrasChanged(this, mExtras);
} else {
Log.e(this, new NullPointerException(),
"putExtras failed due to null CS callId=%s", getId());
}
}
}
/**
* Removes extras from the extras bundle associated with this {@link Call}.
*
* Note: this method needs to know the source of the extras change (see
* {@link #SOURCE_CONNECTION_SERVICE}, {@link #SOURCE_INCALL_SERVICE}). Extras changes which
* originate from a connection service will only be notified to incall services. Likewise,
* changes originating from the incall services will only notify the connection service of the
* change.
*
* @param source The source of the extras removal.
* @param keys The extra keys to remove.
*/
void removeExtras(int source, List<String> keys) {
if (mExtras == null) {
return;
}
for (String key : keys) {
mExtras.remove(key);
}
for (Listener l : mListeners) {
l.onExtrasRemoved(this, source, keys);
}
// If the change originated from an InCallService, notify the connection service.
if (source == SOURCE_INCALL_SERVICE) {
if (mConnectionService != null) {
mConnectionService.onExtrasChanged(this, mExtras);
} else {
Log.e(this, new NullPointerException(),
"removeExtras failed due to null CS callId=%s", getId());
}
}
}
@VisibleForTesting
public Bundle getIntentExtras() {
return mIntentExtras;
}
void setIntentExtras(Bundle extras) {
mIntentExtras = extras;
}
public Intent getOriginalCallIntent() {
return mOriginalCallIntent;
}
public void setOriginalCallIntent(Intent intent) {
mOriginalCallIntent = intent;
}
/**
* @return the uri of the contact associated with this call.
*/
@VisibleForTesting
public Uri getContactUri() {
if (mCallerInfo == null || !mCallerInfo.contactExists) {
return getHandle();
}
return Contacts.getLookupUri(mCallerInfo.getContactId(), mCallerInfo.lookupKey);
}
Uri getRingtone() {
return mCallerInfo == null ? null : mCallerInfo.contactRingtoneUri;
}
void onPostDialWait(String remaining) {
for (Listener l : mListeners) {
l.onPostDialWait(this, remaining);
}
}
void onPostDialChar(char nextChar) {
for (Listener l : mListeners) {
l.onPostDialChar(this, nextChar);
}
}
void postDialContinue(boolean proceed) {
if (mConnectionService != null) {
mConnectionService.onPostDialContinue(this, proceed);
} else {
Log.e(this, new NullPointerException(),
"postDialContinue failed due to null CS callId=%s", getId());
}
}
void conferenceWith(Call otherCall) {
if (mConnectionService == null) {
Log.w(this, "conference requested on a call without a connection service.");
} else {
Log.addEvent(this, LogUtils.Events.CONFERENCE_WITH, otherCall);
mConnectionService.conference(this, otherCall);
}
}
void splitFromConference() {
if (mConnectionService == null) {
Log.w(this, "splitting from conference call without a connection service");
} else {
Log.addEvent(this, LogUtils.Events.SPLIT_FROM_CONFERENCE);
mConnectionService.splitFromConference(this);
}
}
@VisibleForTesting
public void mergeConference() {
if (mConnectionService == null) {
Log.w(this, "merging conference calls without a connection service.");
} else if (can(Connection.CAPABILITY_MERGE_CONFERENCE)) {
Log.addEvent(this, LogUtils.Events.CONFERENCE_WITH);
mConnectionService.mergeConference(this);
mWasConferencePreviouslyMerged = true;
}
}
@VisibleForTesting
public void swapConference() {
if (mConnectionService == null) {
Log.w(this, "swapping conference calls without a connection service.");
} else if (can(Connection.CAPABILITY_SWAP_CONFERENCE)) {
Log.addEvent(this, LogUtils.Events.SWAP);
mConnectionService.swapConference(this);
switch (mChildCalls.size()) {
case 1:
mConferenceLevelActiveCall = mChildCalls.get(0);
break;
case 2:
// swap
mConferenceLevelActiveCall = mChildCalls.get(0) == mConferenceLevelActiveCall ?
mChildCalls.get(1) : mChildCalls.get(0);
break;
default:
// For anything else 0, or 3+, set it to null since it is impossible to tell.
mConferenceLevelActiveCall = null;
break;
}
for (Listener l : mListeners) {
l.onCdmaConferenceSwap(this);
}
}
}
public void addConferenceParticipants(List<Uri> participants) {
if (mConnectionService == null) {
Log.w(this, "adding conference participants without a connection service.");
} else if (can(Connection.CAPABILITY_ADD_PARTICIPANT)) {
Log.addEvent(this, LogUtils.Events.ADD_PARTICIPANT);
mConnectionService.addConferenceParticipants(this, participants);
}
}
/**
* Initiates a request to the connection service to pull this call.
* <p>
* This method can only be used for calls that have the
* {@link android.telecom.Connection#CAPABILITY_CAN_PULL_CALL} capability and
* {@link android.telecom.Connection#PROPERTY_IS_EXTERNAL_CALL} property set.
* <p>
* An external call is a representation of a call which is taking place on another device
* associated with a PhoneAccount on this device. Issuing a request to pull the external call
* tells the {@link android.telecom.ConnectionService} that it should move the call from the
* other device to this one. An example of this is the IMS multi-endpoint functionality. A
* user may have two phones with the same phone number. If the user is engaged in an active
* call on their first device, the network will inform the second device of that ongoing call in
* the form of an external call. The user may wish to continue their conversation on the second
* device, so will issue a request to pull the call to the second device.
* <p>
* Requests to pull a call which is not external, or a call which is not pullable are ignored.
* If there is an ongoing emergency call, pull requests are also ignored.
*/
public void pullExternalCall() {
if (mConnectionService == null) {
Log.w(this, "pulling a call without a connection service.");
}
if (!hasProperty(Connection.PROPERTY_IS_EXTERNAL_CALL)) {
Log.w(this, "pullExternalCall - call %s is not an external call.", mId);
return;
}
if (!can(Connection.CAPABILITY_CAN_PULL_CALL)) {
Log.w(this, "pullExternalCall - call %s is external but cannot be pulled.", mId);
return;
}
if (mCallsManager.isInEmergencyCall()) {
Log.w(this, "pullExternalCall = pullExternalCall - call %s is external but can not be"
+ " pulled while an emergency call is in progress.", mId);
mToastFactory.makeText(mContext, R.string.toast_emergency_can_not_pull_call,
Toast.LENGTH_LONG).show();
return;
}
Log.addEvent(this, LogUtils.Events.REQUEST_PULL);
mConnectionService.pullExternalCall(this);
}
/**
* Sends a call event to the {@link ConnectionService} for this call. This function is
* called for event other than {@link Call#EVENT_REQUEST_HANDOVER}
*
* @param event The call event.
* @param extras Associated extras.
*/
public void sendCallEvent(String event, Bundle extras) {
sendCallEvent(event, 0/*For Event != EVENT_REQUEST_HANDOVER*/, extras);
}
/**
* Sends a call event to the {@link ConnectionService} for this call.
*
* See {@link Call#sendCallEvent(String, Bundle)}.
*
* @param event The call event.
* @param targetSdkVer SDK version of the app calling this api
* @param extras Associated extras.
*/
public void sendCallEvent(String event, int targetSdkVer, Bundle extras) {
if (mConnectionService != null) {
if (android.telecom.Call.EVENT_REQUEST_HANDOVER.equals(event)) {
if (targetSdkVer > Build.VERSION_CODES.P) {
Log.e(this, new Exception(), "sendCallEvent failed. Use public api handoverTo" +
" for API > 28(P)");
// Event-based Handover APIs are deprecated, so inform the user.
mHandler.post(new Runnable() {
@Override
public void run() {
mToastFactory.makeText(mContext,
"WARNING: Event-based handover APIs are deprecated and will no"
+ " longer function in Android Q.",
Toast.LENGTH_LONG).show();
}
});
// Uncomment and remove toast at feature complete: return;
}
// Handover requests are targeted at Telecom, not the ConnectionService.
if (extras == null) {
Log.w(this, "sendCallEvent: %s event received with null extras.",
android.telecom.Call.EVENT_REQUEST_HANDOVER);
mConnectionService.sendCallEvent(this,
android.telecom.Call.EVENT_HANDOVER_FAILED, null);
return;
}
Parcelable parcelable = extras.getParcelable(
android.telecom.Call.EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE);
if (!(parcelable instanceof PhoneAccountHandle) || parcelable == null) {
Log.w(this, "sendCallEvent: %s event received with invalid handover acct.",
android.telecom.Call.EVENT_REQUEST_HANDOVER);
mConnectionService.sendCallEvent(this,
android.telecom.Call.EVENT_HANDOVER_FAILED, null);
return;
}
PhoneAccountHandle phoneAccountHandle = (PhoneAccountHandle) parcelable;
int videoState = extras.getInt(android.telecom.Call.EXTRA_HANDOVER_VIDEO_STATE,
VideoProfile.STATE_AUDIO_ONLY);
Parcelable handoverExtras = extras.getParcelable(
android.telecom.Call.EXTRA_HANDOVER_EXTRAS);
Bundle handoverExtrasBundle = null;
if (handoverExtras instanceof Bundle) {
handoverExtrasBundle = (Bundle) handoverExtras;
}
requestHandover(phoneAccountHandle, videoState, handoverExtrasBundle, true);
} else {
// Relay bluetooth call quality reports to the call diagnostic service.
if (BluetoothCallQualityReport.EVENT_BLUETOOTH_CALL_QUALITY_REPORT.equals(event)
&& extras.containsKey(
BluetoothCallQualityReport.EXTRA_BLUETOOTH_CALL_QUALITY_REPORT)) {
notifyBluetoothCallQualityReport(extras.getParcelable(
BluetoothCallQualityReport.EXTRA_BLUETOOTH_CALL_QUALITY_REPORT
));
}
Log.addEvent(this, LogUtils.Events.CALL_EVENT, event);
mConnectionService.sendCallEvent(this, event, extras);
}
} else {
Log.e(this, new NullPointerException(),
"sendCallEvent failed due to null CS callId=%s", getId());
}
}
/**
* Notifies listeners when a bluetooth quality report is received.
* @param report The bluetooth quality report.
*/
void notifyBluetoothCallQualityReport(@NonNull BluetoothCallQualityReport report) {
Log.addEvent(this, LogUtils.Events.BT_QUALITY_REPORT, "choppy=" + report.isChoppyVoice());
for (Listener l : mListeners) {
l.onBluetoothCallQualityReport(this, report);
}
}
/**
* Initiates a handover of this Call to the {@link ConnectionService} identified
* by destAcct.
* @param destAcct ConnectionService to which the call should be handed over.
* @param videoState The video state desired after the handover.
* @param extras Extra information to be passed to ConnectionService
*/
public void handoverTo(PhoneAccountHandle destAcct, int videoState, Bundle extras) {
requestHandover(destAcct, videoState, extras, false);
}
/**
* Sets this {@link Call} to has the specified {@code parentCall}. Also sets the parent to
* have this call as a child.
* @param parentCall
*/
void setParentAndChildCall(Call parentCall) {
boolean isParentChanging = (mParentCall != parentCall);
setParentCall(parentCall);
setChildOf(parentCall);
if (isParentChanging) {
notifyParentChanged(parentCall);
}
}
/**
* Notifies listeners when the parent call changes.
* Used by {@link #setParentAndChildCall(Call)}, and in {@link CallsManager}.
* @param parentCall The new parent call for this call.
*/
void notifyParentChanged(Call parentCall) {
Log.addEvent(this, LogUtils.Events.SET_PARENT, parentCall);
for (Listener l : mListeners) {
l.onParentChanged(this);
}
}
/**
* Unlike {@link #setParentAndChildCall(Call)}, only sets the parent call but does NOT set
* the child.
* TODO: This is only required when adding existing connections as a workaround so that we
* can avoid sending the "onParentChanged" callback until later.
* @param parentCall The new parent call.
*/
void setParentCall(Call parentCall) {
if (parentCall == this) {
Log.e(this, new Exception(), "setting the parent to self");
return;
}
if (parentCall == mParentCall) {
// nothing to do
return;
}
if (mParentCall != null) {
mParentCall.removeChildCall(this);
}
mParentCall = parentCall;
}
/**
* To be called after {@link #setParentCall(Call)} to complete setting the parent by adding
* this call as a child of another call.
* <p>
* Note: if using this method alone, the caller must call {@link #notifyParentChanged(Call)} to
* ensure the InCall UI is updated with the change in parent.
* @param parentCall The new parent for this call.
*/
public void setChildOf(Call parentCall) {
if (parentCall != null && !parentCall.getChildCalls().contains(this)) {
parentCall.addChildCall(this);
}
}
void setConferenceableCalls(List<Call> conferenceableCalls) {
mConferenceableCalls.clear();
mConferenceableCalls.addAll(conferenceableCalls);
for (Listener l : mListeners) {
l.onConferenceableCallsChanged(this);
}
}
@VisibleForTesting
public List<Call> getConferenceableCalls() {
return mConferenceableCalls;
}
@VisibleForTesting
public boolean can(int capability) {
return (getConnectionCapabilities() & capability) == capability;
}
@VisibleForTesting
public boolean hasProperty(int property) {
return (mConnectionProperties & property) == property;
}
private void addChildCall(Call call) {
if (!mChildCalls.contains(call)) {
mHadChildren = true;
// Set the pseudo-active call to the latest child added to the conference.
// See definition of mConferenceLevelActiveCall for more detail.
mConferenceLevelActiveCall = call;
mChildCalls.add(call);
// When adding a child, we will potentially adjust the various times from the calls
// based on the children being added. This ensures the parent of the conference has a
// connect time reflective of all the children added.
maybeAdjustConnectTime(call);
Log.addEvent(this, LogUtils.Events.ADD_CHILD, call);
for (Listener l : mListeners) {
l.onChildrenChanged(this);
}
}
}
/**
* Potentially adjust the connect and creation time of this call based on another one.
* Ensures that if the other call has an earlier connect time that we adjust the connect time of
* this call to match.
* <p>
* This is important for conference calls; as we add children to the conference we need to
* ensure that earlier connect time is reflected on the conference. In the past this
* was just done in {@link ParcelableCallUtils} when parceling the calls to the UI, but that
* approach would not reflect the right time on the parent as children disconnect.
*
* @param call the call to potentially use to adjust connect time.
*/
private void maybeAdjustConnectTime(@NonNull Call call) {
long childConnectTimeMillis = call.getConnectTimeMillis();
long currentConnectTimeMillis = getConnectTimeMillis();
// Conference calls typically have a 0 connect time, so we will replace the current connect
// time if its zero also.
if (childConnectTimeMillis != 0
&& (currentConnectTimeMillis == 0
|| childConnectTimeMillis < getConnectTimeMillis())) {
setConnectTimeMillis(childConnectTimeMillis);
}
}
private void removeChildCall(Call call) {
if (mChildCalls.remove(call)) {
Log.addEvent(this, LogUtils.Events.REMOVE_CHILD, call);
for (Listener l : mListeners) {
l.onChildrenChanged(this);
}
}
}
/**
* Return whether the user can respond to this {@code Call} via an SMS message.
*
* @return true if the "Respond via SMS" feature should be enabled
* for this incoming call.
*
* The general rule is that we *do* allow "Respond via SMS" except for
* the few (relatively rare) cases where we know for sure it won't
* work, namely:
* - a bogus or blank incoming number
* - a call from a SIP address
* - a "call presentation" that doesn't allow the number to be revealed
*
* In all other cases, we allow the user to respond via SMS.
*
* Note that this behavior isn't perfect; for example we have no way
* to detect whether the incoming call is from a landline (with most
* networks at least), so we still enable this feature even though
* SMSes to that number will silently fail.
*/
public boolean isRespondViaSmsCapable() {
if (mState != CallState.RINGING) {
return false;
}
if (getHandle() == null) {
// No incoming number known or call presentation is "PRESENTATION_RESTRICTED", in
// other words, the user should not be able to see the incoming phone number.
return false;
}
if (mPhoneNumberUtilsAdapter.isUriNumber(getHandle().toString())) {
// The incoming number is actually a URI (i.e. a SIP address),
// not a regular PSTN phone number, and we can't send SMSes to
// SIP addresses.
// (TODO: That might still be possible eventually, though. Is
// there some SIP-specific equivalent to sending a text message?)
return false;
}
// Is there a valid SMS application on the phone?
if (mContext.getSystemService(TelephonyManager.class)
.getAndUpdateDefaultRespondViaMessageApplication() == null) {
return false;
}
// TODO: with some carriers (in certain countries) you *can* actually
// tell whether a given number is a mobile phone or not. So in that
// case we could potentially return false here if the incoming call is
// from a land line.
// If none of the above special cases apply, it's OK to enable the
// "Respond via SMS" feature.
return true;
}
List<String> getCannedSmsResponses() {
return mCannedSmsResponses;
}
/**
* We need to make sure that before we move a call to the disconnected state, it no
* longer has any parent/child relationships. We want to do this to ensure that the InCall
* Service always has the right data in the right order. We also want to do it in telecom so
* that the insurance policy lives in the framework side of things.
*/
private void fixParentAfterDisconnect() {
setParentAndChildCall(null);
}
/**
* @return True if the call is ringing, else logs the action name.
*/
private boolean isRinging(String actionName) {
if (mState == CallState.RINGING || mState == CallState.ANSWERED) {
return true;
}
Log.i(this, "Request to %s a non-ringing call %s", actionName, this);
return false;
}
/**
* @return True if the call is answered, else logs the action name.
*/
private boolean isAnswered(String actionName) {
if (mState == CallState.ANSWERED) {
return true;
}
Log.i(this, "Request to %s a non-answered call %s", actionName, this);
return false;
}
@SuppressWarnings("rawtypes")
private void decrementAssociatedCallCount(ServiceBinder binder) {
if (binder != null) {
binder.decrementAssociatedCallCount();
}
}
/**
* Looks up contact information based on the current handle.
*/
private void startCallerInfoLookup() {
mCallerInfo = null;
mCallsManager.getCallerInfoLookupHelper().startLookup(mHandle, mCallerInfoQueryListener);
}
/**
* Saves the specified caller info if the specified token matches that of the last query
* that was made.
*
* @param callerInfo The new caller information to set.
*/
private void setCallerInfo(Uri handle, CallerInfo callerInfo) {
Trace.beginSection("setCallerInfo");
if (callerInfo == null) {
Log.i(this, "CallerInfo lookup returned null, skipping update");
return;
}
if ((handle != null) && !handle.equals(mHandle)) {
Log.i(this, "setCallerInfo received stale caller info for an old handle. Ignoring.");
return;
}
mCallerInfo = callerInfo;
Log.i(this, "CallerInfo received for %s: %s", Log.piiHandle(mHandle), callerInfo);
if (mCallerInfo.getContactDisplayPhotoUri() == null ||
mCallerInfo.cachedPhotoIcon != null || mCallerInfo.cachedPhoto != null) {
for (Listener l : mListeners) {
l.onCallerInfoChanged(this);
}
}
Trace.endSection();
}
public CallerInfo getCallerInfo() {
return mCallerInfo;
}
private void maybeLoadCannedSmsResponses() {
if (mCallDirection == CALL_DIRECTION_INCOMING
&& isRespondViaSmsCapable()
&& !mCannedSmsResponsesLoadingStarted) {
Log.d(this, "maybeLoadCannedSmsResponses: starting task to load messages");
mCannedSmsResponsesLoadingStarted = true;
mCallsManager.getRespondViaSmsManager().loadCannedTextMessages(
new Response<Void, List<String>>() {
@Override
public void onResult(Void request, List<String>... result) {
if (result.length > 0) {
Log.d(this, "maybeLoadCannedSmsResponses: got %s", result[0]);
mCannedSmsResponses = result[0];
for (Listener l : mListeners) {
l.onCannedSmsResponsesLoaded(Call.this);
}
}
}
@Override
public void onError(Void request, int code, String msg) {
Log.w(Call.this, "Error obtaining canned SMS responses: %d %s", code,
msg);
}
},
mContext
);
} else {
Log.d(this, "maybeLoadCannedSmsResponses: doing nothing");
}
}
/**
* Sets speakerphone option on when call begins.
*/
public void setStartWithSpeakerphoneOn(boolean startWithSpeakerphone) {
mSpeakerphoneOn = startWithSpeakerphone;
}
/**
* Returns speakerphone option.
*
* @return Whether or not speakerphone should be set automatically when call begins.
*/
public boolean getStartWithSpeakerphoneOn() {
return mSpeakerphoneOn;
}
public void setRequestedToStartWithRtt() {
mDidRequestToStartWithRtt = true;
}
public void stopRtt() {
if (mConnectionService != null) {
mConnectionService.stopRtt(this);
} else {
// If this gets called by the in-call app before the connection service is set, we'll
// just ignore it since it's really not supposed to happen.
Log.w(this, "stopRtt() called before connection service is set.");
}
}
public void sendRttRequest() {
createRttStreams();
mConnectionService.startRtt(this, getInCallToCsRttPipeForCs(), getCsToInCallRttPipeForCs());
}
private boolean areRttStreamsInitialized() {
return mInCallToConnectionServiceStreams != null
&& mConnectionServiceToInCallStreams != null;
}
public void createRttStreams() {
if (!areRttStreamsInitialized()) {
Log.i(this, "Initializing RTT streams");
try {
mInCallToConnectionServiceStreams = ParcelFileDescriptor.createReliablePipe();
mConnectionServiceToInCallStreams = ParcelFileDescriptor.createReliablePipe();
} catch (IOException e) {
Log.e(this, e, "Failed to create pipes for RTT call.");
}
}
}
public void onRttConnectionFailure(int reason) {
Log.i(this, "Got RTT initiation failure with reason %d", reason);
for (Listener l : mListeners) {
l.onRttInitiationFailure(this, reason);
}
}
public void onRemoteRttRequest() {
if (isRttCall()) {
Log.w(this, "Remote RTT request on a call that's already RTT");
return;
}
mPendingRttRequestId = mCallsManager.getNextRttRequestId();
for (Listener l : mListeners) {
l.onRemoteRttRequest(this, mPendingRttRequestId);
}
}
public void handleRttRequestResponse(int id, boolean accept) {
if (mPendingRttRequestId == INVALID_RTT_REQUEST_ID) {
Log.w(this, "Response received to a nonexistent RTT request: %d", id);
return;
}
if (id != mPendingRttRequestId) {
Log.w(this, "Response ID %d does not match expected %d", id, mPendingRttRequestId);
return;
}
if (accept) {
createRttStreams();
Log.i(this, "RTT request %d accepted.", id);
mConnectionService.respondToRttRequest(
this, getInCallToCsRttPipeForCs(), getCsToInCallRttPipeForCs());
} else {
Log.i(this, "RTT request %d rejected.", id);
mConnectionService.respondToRttRequest(this, null, null);
}
}
public boolean isRttCall() {
return (mConnectionProperties & Connection.PROPERTY_IS_RTT) == Connection.PROPERTY_IS_RTT;
}
public boolean wasEverRttCall() {
return mWasEverRtt;
}
public ParcelFileDescriptor getCsToInCallRttPipeForCs() {
return mConnectionServiceToInCallStreams == null ? null
: mConnectionServiceToInCallStreams[RTT_PIPE_WRITE_SIDE_INDEX];
}
public ParcelFileDescriptor getInCallToCsRttPipeForCs() {
return mInCallToConnectionServiceStreams == null ? null
: mInCallToConnectionServiceStreams[RTT_PIPE_READ_SIDE_INDEX];
}
public ParcelFileDescriptor getCsToInCallRttPipeForInCall() {
return mConnectionServiceToInCallStreams == null ? null
: mConnectionServiceToInCallStreams[RTT_PIPE_READ_SIDE_INDEX];
}
public ParcelFileDescriptor getInCallToCsRttPipeForInCall() {
return mInCallToConnectionServiceStreams == null ? null
: mInCallToConnectionServiceStreams[RTT_PIPE_WRITE_SIDE_INDEX];
}
public int getRttMode() {
return mRttMode;
}
/**
* Sets a video call provider for the call.
*/
public void setVideoProvider(IVideoProvider videoProvider) {
Log.v(this, "setVideoProvider");
if (mVideoProviderProxy != null) {
mVideoProviderProxy.clearVideoCallback();
mVideoProviderProxy = null;
}
if (videoProvider != null ) {
try {
mVideoProviderProxy = new VideoProviderProxy(mLock, videoProvider, this,
mCallsManager);
} catch (RemoteException ignored) {
// Ignore RemoteException.
}
}
mVideoProvider = videoProvider;
for (Listener l : mListeners) {
l.onVideoCallProviderChanged(Call.this);
}
}
/**
* @return The {@link Connection.VideoProvider} binder.
*/
public IVideoProvider getVideoProvider() {
if (mVideoProviderProxy == null) {
return null;
}
return mVideoProviderProxy.getInterface();
}
/**
* @return The {@link VideoProviderProxy} for this call.
*/
public VideoProviderProxy getVideoProviderProxy() {
return mVideoProviderProxy;
}
/**
* The current video state for the call.
* See {@link VideoProfile} for a list of valid video states.
*/
public int getVideoState() {
return mVideoState;
}
/**
* Returns the video states which were applicable over the duration of a call.
* See {@link VideoProfile} for a list of valid video states.
*
* @return The video states applicable over the duration of the call.
*/
public int getVideoStateHistory() {
return mVideoStateHistory;
}
/**
* Determines the current video state for the call.
* For an outgoing call determines the desired video state for the call.
* Valid values: see {@link VideoProfile}
*
* @param videoState The video state for the call.
*/
public void setVideoState(int videoState) {
// If the phone account associated with this call does not support video calling, then we
// will automatically set the video state to audio-only.
if (!isVideoCallingSupportedByPhoneAccount()) {
Log.d(this, "setVideoState: videoState=%s defaulted to audio (video not supported)",
VideoProfile.videoStateToString(videoState));
videoState = VideoProfile.STATE_AUDIO_ONLY;
}
// Track Video State history during the duration of the call.
// Only update the history when the call is active or disconnected. This ensures we do
// not include the video state history when:
// - Call is incoming (but not answered).
// - Call it outgoing (but not answered).
// We include the video state when disconnected to ensure that rejected calls reflect the
// appropriate video state.
// For all other times we add to the video state history, see #setState.
if (isActive() || getState() == CallState.DISCONNECTED) {
mVideoStateHistory = mVideoStateHistory | videoState;
}
int previousVideoState = mVideoState;
mVideoState = videoState;
if (mVideoState != previousVideoState) {
Log.addEvent(this, LogUtils.Events.VIDEO_STATE_CHANGED,
VideoProfile.videoStateToString(videoState));
for (Listener l : mListeners) {
l.onVideoStateChanged(this, previousVideoState, mVideoState);
}
}
if (VideoProfile.isVideo(videoState)) {
mAnalytics.setCallIsVideo(true);
}
}
public boolean getIsVoipAudioMode() {
return mIsVoipAudioMode;
}
public void setIsVoipAudioMode(boolean audioModeIsVoip) {
mIsVoipAudioMode = audioModeIsVoip;
for (Listener l : mListeners) {
l.onIsVoipAudioModeChanged(this);
}
}
public StatusHints getStatusHints() {
return mStatusHints;
}
public void setStatusHints(StatusHints statusHints) {
mStatusHints = statusHints;
for (Listener l : mListeners) {
l.onStatusHintsChanged(this);
}
}
public boolean isUnknown() {
return mCallDirection == CALL_DIRECTION_UNKNOWN;
}
/**
* Determines if this call is in a disconnecting state.
*
* @return {@code true} if this call is locally disconnecting.
*/
public boolean isLocallyDisconnecting() {
return mIsLocallyDisconnecting;
}
/**
* Sets whether this call is in a disconnecting state.
*
* @param isLocallyDisconnecting {@code true} if this call is locally disconnecting.
*/
private void setLocallyDisconnecting(boolean isLocallyDisconnecting) {
mIsLocallyDisconnecting = isLocallyDisconnecting;
}
/**
* @return user handle of user initiating the outgoing call.
*/
public UserHandle getInitiatingUser() {
return mInitiatingUser;
}
/**
* Set the user handle of user initiating the outgoing call.
* @param initiatingUser
*/
public void setInitiatingUser(UserHandle initiatingUser) {
Preconditions.checkNotNull(initiatingUser);
mInitiatingUser = initiatingUser;
}
static int getStateFromConnectionState(int state) {
switch (state) {
case Connection.STATE_INITIALIZING:
return CallState.CONNECTING;
case Connection.STATE_ACTIVE:
return CallState.ACTIVE;
case Connection.STATE_DIALING:
return CallState.DIALING;
case Connection.STATE_PULLING_CALL:
return CallState.PULLING;
case Connection.STATE_DISCONNECTED:
return CallState.DISCONNECTED;
case Connection.STATE_HOLDING:
return CallState.ON_HOLD;
case Connection.STATE_NEW:
return CallState.NEW;
case Connection.STATE_RINGING:
return CallState.RINGING;
}
return CallState.DISCONNECTED;
}
/**
* Determines if this call is in disconnected state and waiting to be destroyed.
*
* @return {@code true} if this call is disconected.
*/
public boolean isDisconnected() {
return (getState() == CallState.DISCONNECTED || getState() == CallState.ABORTED);
}
/**
* Determines if this call has just been created and has not been configured properly yet.
*
* @return {@code true} if this call is new.
*/
public boolean isNew() {
return getState() == CallState.NEW;
}
/**
* Sets the call data usage for the call.
*
* @param callDataUsage The new call data usage (in bytes).
*/
public void setCallDataUsage(long callDataUsage) {
mCallDataUsage = callDataUsage;
}
/**
* Returns the call data usage for the call.
*
* @return The call data usage (in bytes).
*/
public long getCallDataUsage() {
return mCallDataUsage;
}
public void setRttMode(int mode) {
mRttMode = mode;
// TODO: hook this up to CallAudioManager
}
/**
* Returns true if the call is outgoing and the NEW_OUTGOING_CALL ordered broadcast intent
* has come back to telecom and was processed.
*/
public boolean isNewOutgoingCallIntentBroadcastDone() {
return mIsNewOutgoingCallIntentBroadcastDone;
}
public void setNewOutgoingCallIntentBroadcastIsDone() {
mIsNewOutgoingCallIntentBroadcastDone = true;
}
/**
* Determines if the call has been held by the remote party.
*
* @return {@code true} if the call is remotely held, {@code false} otherwise.
*/
public boolean isRemotelyHeld() {
return mIsRemotelyHeld;
}
/**
* Handles Connection events received from a {@link ConnectionService}.
*
* @param event The event.
* @param extras The extras.
*/
public void onConnectionEvent(String event, Bundle extras) {
// Don't log call quality reports; they're quite frequent and will clog the log.
if (!Connection.EVENT_CALL_QUALITY_REPORT.equals(event)) {
Log.addEvent(this, LogUtils.Events.CONNECTION_EVENT, event);
}
if (Connection.EVENT_ON_HOLD_TONE_START.equals(event)) {
mIsRemotelyHeld = true;
Log.addEvent(this, LogUtils.Events.REMOTELY_HELD);
// Inform listeners of the fact that a call hold tone was received. This will trigger
// the CallAudioManager to play a tone via the InCallTonePlayer.
for (Listener l : mListeners) {
l.onHoldToneRequested(this);
}
} else if (Connection.EVENT_ON_HOLD_TONE_END.equals(event)) {
mIsRemotelyHeld = false;
Log.addEvent(this, LogUtils.Events.REMOTELY_UNHELD);
for (Listener l : mListeners) {
l.onHoldToneRequested(this);
}
} else if (Connection.EVENT_CALL_HOLD_FAILED.equals(event)) {
for (Listener l : mListeners) {
l.onCallHoldFailed(this);
}
} else if (Connection.EVENT_CALL_SWITCH_FAILED.equals(event)) {
for (Listener l : mListeners) {
l.onCallSwitchFailed(this);
}
} else if (Connection.EVENT_DEVICE_TO_DEVICE_MESSAGE.equals(event)
&& extras != null && extras.containsKey(
Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_TYPE)
&& extras.containsKey(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_VALUE)) {
// Relay an incoming D2D message to interested listeners; most notably the
// CallDiagnosticService.
int messageType = extras.getInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_TYPE);
int messageValue = extras.getInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_VALUE);
for (Listener l : mListeners) {
l.onReceivedDeviceToDeviceMessage(this, messageType, messageValue);
}
} else if (Connection.EVENT_CALL_QUALITY_REPORT.equals(event)
&& extras != null && extras.containsKey(Connection.EXTRA_CALL_QUALITY_REPORT)) {
CallQuality callQuality = extras.getParcelable(Connection.EXTRA_CALL_QUALITY_REPORT);
for (Listener l : mListeners) {
l.onReceivedCallQualityReport(this, callQuality);
}
} else {
for (Listener l : mListeners) {
l.onConnectionEvent(this, event, extras);
}
}
}
/**
* Notifies interested parties that the handover has completed.
* Notifies:
* 1. {@link InCallController} which communicates this to the
* {@link android.telecom.InCallService} via {@link Listener#onHandoverComplete()}.
* 2. {@link ConnectionServiceWrapper} which informs the {@link android.telecom.Connection} of
* the successful handover.
*/
public void onHandoverComplete() {
Log.i(this, "onHandoverComplete; callId=%s", getId());
if (mConnectionService != null) {
mConnectionService.handoverComplete(this);
}
for (Listener l : mListeners) {
l.onHandoverComplete(this);
}
}
public void onHandoverFailed(int handoverError) {
Log.i(this, "onHandoverFailed; callId=%s, handoverError=%d", getId(), handoverError);
for (Listener l : mListeners) {
l.onHandoverFailed(this, handoverError);
}
}
public void setOriginalConnectionId(String originalConnectionId) {
mOriginalConnectionId = originalConnectionId;
}
/**
* For calls added via a ConnectionManager using the
* {@link android.telecom.ConnectionService#addExistingConnection(PhoneAccountHandle,
* Connection)}, or {@link android.telecom.ConnectionService#addConference(Conference)} APIS,
* indicates the ID of this call as it was referred to by the {@code ConnectionService} which
* originally created it.
*
* See {@link Connection#EXTRA_ORIGINAL_CONNECTION_ID}.
* @return The original connection ID.
*/
public String getOriginalConnectionId() {
return mOriginalConnectionId;
}
public ConnectionServiceFocusManager getConnectionServiceFocusManager() {
return mCallsManager.getConnectionServiceFocusManager();
}
/**
* Determines if a {@link Call}'s capabilities bitmask indicates that video is supported either
* remotely or locally.
*
* @param capabilities The {@link Connection} capabilities for the call.
* @return {@code true} if video is supported, {@code false} otherwise.
*/
private boolean doesCallSupportVideo(int capabilities) {
return (capabilities & Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL) != 0 ||
(capabilities & Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL) != 0;
}
/**
* Remove any video capabilities set on a {@link Connection} capabilities bitmask.
*
* @param capabilities The capabilities.
* @return The bitmask with video capabilities removed.
*/
private int removeVideoCapabilities(int capabilities) {
return capabilities & ~(Connection.CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL |
Connection.CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL);
}
/**
* Initiates a handover of this {@link Call} to another {@link PhoneAccount}.
* @param handoverToHandle The {@link PhoneAccountHandle} to handover to.
* @param videoState The video state of the call when handed over.
* @param extras Optional extras {@link Bundle} provided by the initiating
* {@link android.telecom.InCallService}.
*/
private void requestHandover(PhoneAccountHandle handoverToHandle, int videoState,
Bundle extras, boolean isLegacy) {
for (Listener l : mListeners) {
l.onHandoverRequested(this, handoverToHandle, videoState, extras, isLegacy);
}
}
private TelephonyManager getTelephonyManager() {
return mContext.getSystemService(TelephonyManager.class);
}
/**
* Sets whether this {@link Call} is a conference or not.
* @param isConference
*/
public void setConferenceState(boolean isConference) {
mIsConference = isConference;
Log.addEvent(this, LogUtils.Events.CONF_STATE_CHANGED, "isConference=" + isConference);
// Ultimately CallsManager needs to know so it can update the "add call" state and inform
// the UI to update itself.
for (Listener l : mListeners) {
l.onConferenceStateChanged(this, isConference);
}
}
/**
* Change the call direction. This is useful if it was not previously defined (for example in
* single caller emulation mode).
* @param callDirection The new direction of this call.
*/
// Make sure the callDirection has been mapped to the Call definition correctly!
public void setCallDirection(int callDirection) {
if (mCallDirection != callDirection) {
Log.addEvent(this, LogUtils.Events.CALL_DIRECTION_CHANGED, "callDirection="
+ callDirection);
mCallDirection = callDirection;
for (Listener l : mListeners) {
// Update InCallService directly, do not notify CallsManager.
l.onCallDirectionChanged(this);
}
}
}
/**
* Sets the video history based on the state and state transitions of the call. Always add the
* current video state to the video state history during a call transition except for the
* transitions DIALING->ACTIVE and RINGING->ANSWERED. In these cases, clear the history. If a
* call starts dialing/ringing as a VT call and gets downgraded to audio, we need to record
* the history as an audio call.
*/
private void updateVideoHistoryViaState(int oldState, int newState) {
if ((oldState == CallState.DIALING && newState == CallState.ACTIVE)
|| (oldState == CallState.RINGING && newState == CallState.ANSWERED)) {
mVideoStateHistory = mVideoState;
}
mVideoStateHistory |= mVideoState;
}
/**
* Returns whether or not high definition audio was used.
*
* @return true if high definition audio was used during this call.
*/
boolean wasHighDefAudio() {
return mWasHighDefAudio;
}
/**
* Returns whether or not Wifi call was used.
*
* @return true if wifi call was used during this call.
*/
boolean wasWifi() {
return mWasWifi;
}
public void setIsUsingCallFiltering(boolean isUsingCallFiltering) {
mIsUsingCallFiltering = isUsingCallFiltering;
}
public boolean isUsingCallFiltering() {
return mIsUsingCallFiltering;
}
/**
* Returns whether or not Volte call was used.
*
* @return true if Volte call was used during this call.
*/
public boolean wasVolte() {
return mWasVolte;
}
/**
* In some cases, we need to know if this call has ever gone active (for example, the case
* when the call was put into the {@link CallState#AUDIO_PROCESSING} state after being active)
* for call logging purposes.
*
* @return {@code true} if this call has gone active before (even if it isn't now), false if it
* has never gone active.
*/
public boolean hasGoneActiveBefore() {
return mHasGoneActiveBefore;
}
/**
* When upgrading a call to video via
* {@link VideoProviderProxy#onSendSessionModifyRequest(VideoProfile, VideoProfile)}, if the
* upgrade is from audio to video, potentially auto-engage the speakerphone.
* @param newVideoState The proposed new video state for the call.
*/
public void maybeEnableSpeakerForVideoUpgrade(@VideoProfile.VideoState int newVideoState) {
if (mCallsManager.isSpeakerphoneAutoEnabledForVideoCalls(newVideoState)) {
Log.i(this, "maybeEnableSpeakerForVideoCall; callId=%s, auto-enable speaker for call"
+ " upgraded to video.");
mCallsManager.setAudioRoute(CallAudioState.ROUTE_SPEAKER, null);
}
}
/**
* Sends a device to device message to the other part of the call.
* @param message the message type to send.
* @param value the value for the message.
*/
public void sendDeviceToDeviceMessage(@CallDiagnostics.MessageType int message, int value) {
Log.i(this, "sendDeviceToDeviceMessage; callId=%s, msg=%d/%d", getId(), message, value);
Bundle extras = new Bundle();
extras.putInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_TYPE, message);
extras.putInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_VALUE, value);
// Send to the connection service.
sendCallEvent(Connection.EVENT_DEVICE_TO_DEVICE_MESSAGE, extras);
}
/**
* Signals to the Dialer app to start displaying a diagnostic message.
* @param messageId a unique ID for the message to display.
* @param message the message to display.
*/
public void displayDiagnosticMessage(int messageId, @NonNull CharSequence message) {
Bundle extras = new Bundle();
extras.putInt(android.telecom.Call.EXTRA_DIAGNOSTIC_MESSAGE_ID, messageId);
extras.putCharSequence(android.telecom.Call.EXTRA_DIAGNOSTIC_MESSAGE, message);
// Send to the dialer.
onConnectionEvent(android.telecom.Call.EVENT_DISPLAY_DIAGNOSTIC_MESSAGE, extras);
}
/**
* Signals to the Dialer app to stop displaying a diagnostic message.
* @param messageId a unique ID for the message to clear.
*/
public void clearDiagnosticMessage(int messageId) {
Bundle extras = new Bundle();
extras.putInt(android.telecom.Call.EXTRA_DIAGNOSTIC_MESSAGE_ID, messageId);
// Send to the dialer.
onConnectionEvent(android.telecom.Call.EVENT_CLEAR_DIAGNOSTIC_MESSAGE, extras);
}
/**
* Remaps the call direction as indicated by an {@link android.telecom.Call.Details} direction
* constant to the constants (e.g. {@link #CALL_DIRECTION_INCOMING}) used in this call class.
* @param direction The android.telecom.Call direction.
* @return The direction using the constants in this class.
*/
public static int getRemappedCallDirection(
@android.telecom.Call.Details.CallDirection int direction) {
switch(direction) {
case android.telecom.Call.Details.DIRECTION_INCOMING:
return CALL_DIRECTION_INCOMING;
case android.telecom.Call.Details.DIRECTION_OUTGOING:
return CALL_DIRECTION_OUTGOING;
case android.telecom.Call.Details.DIRECTION_UNKNOWN:
return CALL_DIRECTION_UNDEFINED;
}
return CALL_DIRECTION_UNDEFINED;
}
/**
* Set the package name of the {@link android.telecom.CallScreeningService} which should be sent
* the {@link android.telecom.TelecomManager#ACTION_POST_CALL} upon disconnection of a call.
* @param packageName post call screen service package name.
*/
public void setPostCallPackageName(String packageName) {
mPostCallPackageName = packageName;
}
/**
* Return the package name of the {@link android.telecom.CallScreeningService} which should be
* sent the {@link android.telecom.TelecomManager#ACTION_POST_CALL} upon disconnection of a
* call.
* @return post call screen service package name.
*/
public String getPostCallPackageName() {
return mPostCallPackageName;
}
public long getMissedReason() {
return mMissedReason;
}
public void setMissedReason(long missedReason) {
mMissedReason = missedReason;
}
public void setUserMissed(long code) {
mMissedReason |= code;
}
public long getStartRingTime() {
return mStartRingTime;
}
public void setStartRingTime(long startRingTime) {
mStartRingTime = startRingTime;
}
public CharSequence getCallScreeningAppName() {
return mCallScreeningAppName;
}
public void setCallScreeningAppName(CharSequence callScreeningAppName) {
mCallScreeningAppName = callScreeningAppName;
}
public String getCallScreeningComponentName() {
return mCallScreeningComponentName;
}
public void setCallScreeningComponentName(String callScreeningComponentName) {
mCallScreeningComponentName = callScreeningComponentName;
}
public void maybeOnInCallServiceTrackingChanged(boolean isTracking, boolean hasUi) {
if (mConnectionService == null) {
Log.w(this, "maybeOnInCallServiceTrackingChanged() request on a call"
+ " without a connection service.");
} else {
if (hasUi) {
mConnectionService.onUsingAlternativeUi(this, isTracking);
} else if (isTracking) {
mConnectionService.onTrackedByNonUiService(this, isTracking);
}
}
}
/**
* @return {@code true} when this call originated from a SIM-based {@link PhoneAccount}.
* A sim-based {@link PhoneAccount} is one with {@link PhoneAccount#CAPABILITY_SIM_SUBSCRIPTION}
* set.
*/
public boolean isSimCall() {
return mIsSimCall;
}
/**
* Sets whether this is a sim call or not.
* @param isSimCall {@code true} if this is a SIM call, {@code false} otherwise.
*/
public void setIsSimCall(boolean isSimCall) {
mIsSimCall = isSimCall;
}
/**
* Initializes a disconnect future which is used to chain up pending operations which take
* place when the {@link CallDiagnosticService} returns the result of the
* {@link CallDiagnostics#onCallDisconnected(int, int)} or
* {@link CallDiagnostics#onCallDisconnected(ImsReasonInfo)} invocation via
* {@link CallDiagnosticServiceAdapter}. If no {@link CallDiagnosticService} is in use, we
* would not try to make a disconnect future.
* @param timeoutMillis Timeout we use for waiting for the response.
* @return the {@link CompletableFuture}.
*/
public CompletableFuture<Boolean> initializeDisconnectFuture(long timeoutMillis) {
if (mDisconnectFuture == null) {
mDisconnectFuture = new CompletableFuture<Boolean>()
.completeOnTimeout(false, timeoutMillis, TimeUnit.MILLISECONDS);
// After all the chained stuff we will report where the CDS timed out.
mDisconnectFuture.thenRunAsync(() -> {
if (!mReceivedCallDiagnosticPostCallResponse) {
Log.addEvent(this, LogUtils.Events.CALL_DIAGNOSTIC_SERVICE_TIMEOUT);
}
// Clear the future as a final step.
mDisconnectFuture = null;
},
new LoggedHandlerExecutor(mHandler, "C.iDF", mLock))
.exceptionally((throwable) -> {
Log.e(this, throwable, "Error while executing disconnect future");
return null;
});
}
return mDisconnectFuture;
}
/**
* @return the disconnect future, if initialized. Used for chaining operations after creation.
*/
public CompletableFuture<Boolean> getDisconnectFuture() {
return mDisconnectFuture;
}
/**
* @return {@code true} if disconnection and removal is handled via a future, or {@code false}
* if this is handled immediately.
*/
public boolean isDisconnectHandledViaFuture() {
return mDisconnectFuture != null;
}
/**
* Perform any cleanup on this call as a result of a {@link TelecomServiceImpl}
* {@code cleanupStuckCalls} request.
*/
public void cleanup() {
if (mDisconnectFuture != null) {
mDisconnectFuture.complete(false);
mDisconnectFuture = null;
}
}
}