blob: bd8f006dd09575167b784f611a8e68fa766eb33a [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.incallui.call;
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Trace;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.telecom.Call;
import android.telecom.Call.Details;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.GatewayInfo;
import android.telecom.InCallService.VideoCall;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import com.android.contacts.common.compat.CallCompat;
import com.android.contacts.common.compat.TelephonyManagerCompat;
import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
import com.android.dialer.callintent.CallIntentParser;
import com.android.dialer.callintent.nano.CallInitiationType;
import com.android.dialer.callintent.nano.CallSpecificAppData;
import com.android.dialer.common.Assert;
import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
import com.android.dialer.logging.nano.ContactLookupResult;
import com.android.dialer.util.CallUtil;
import com.android.incallui.latencyreport.LatencyReport;
import com.android.incallui.util.TelecomCallUtil;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
/** Describes a single call and its state. */
public class DialerCall {
public static final int CALL_HISTORY_STATUS_UNKNOWN = 0;
public static final int CALL_HISTORY_STATUS_PRESENT = 1;
public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2;
private static final String ID_PREFIX = "DialerCall_";
private static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS =
"emergency_callback_window_millis";
private static int sIdCounter = 0;
/**
* The unique call ID for every call. This will help us to identify each call and allow us the
* ability to stitch impressions to calls if needed.
*/
private final String uniqueCallId = UUID.randomUUID().toString();
private final Call mTelecomCall;
private final LatencyReport mLatencyReport;
private final String mId;
private final List<String> mChildCallIds = new ArrayList<>();
private final VideoSettings mVideoSettings = new VideoSettings();
private final LogState mLogState = new LogState();
private final Context mContext;
private final DialerCallDelegate mDialerCallDelegate;
private final List<DialerCallListener> mListeners = new CopyOnWriteArrayList<>();
private final List<CannedTextResponsesLoadedListener> mCannedTextResponsesLoadedListeners =
new CopyOnWriteArrayList<>();
private boolean mIsEmergencyCall;
private Uri mHandle;
private int mState = State.INVALID;
private DisconnectCause mDisconnectCause;
private boolean hasShownWiFiToLteHandoverToast;
private boolean doNotShowDialogForHandoffToWifiFailure;
@SessionModificationState private int mSessionModificationState;
private int mVideoState;
/** mRequestedVideoState is used to store requested upgrade / downgrade video state */
private int mRequestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
private InCallVideoCallCallback mVideoCallCallback;
private boolean mIsVideoCallCallbackRegistered;
private String mChildNumber;
private String mLastForwardedNumber;
private String mCallSubject;
private PhoneAccountHandle mPhoneAccountHandle;
@CallHistoryStatus private int mCallHistoryStatus = CALL_HISTORY_STATUS_UNKNOWN;
private boolean mIsSpam;
private boolean mIsBlocked;
private boolean isInUserSpamList;
private boolean isInUserWhiteList;
private boolean isInGlobalSpamList;
private boolean didShowCameraPermission;
private String callProviderLabel;
private String callbackNumber;
public static String getNumberFromHandle(Uri handle) {
return handle == null ? "" : handle.getSchemeSpecificPart();
}
/**
* Whether the call is put on hold by remote party. This is different than the {@link
* State.ONHOLD} state which indicates that the call is being held locally on the device.
*/
private boolean isRemotelyHeld;
/**
* Indicates whether the phone account associated with this call supports specifying a call
* subject.
*/
private boolean mIsCallSubjectSupported;
private final Call.Callback mTelecomCallCallback =
new Call.Callback() {
@Override
public void onStateChanged(Call call, int newState) {
LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call + " newState=" + newState);
update();
}
@Override
public void onParentChanged(Call call, Call newParent) {
LogUtil.v(
"TelecomCallCallback.onParentChanged", "call=" + call + " newParent=" + newParent);
update();
}
@Override
public void onChildrenChanged(Call call, List<Call> children) {
update();
}
@Override
public void onDetailsChanged(Call call, Call.Details details) {
LogUtil.v("TelecomCallCallback.onStateChanged", " call=" + call + " details=" + details);
update();
}
@Override
public void onCannedTextResponsesLoaded(Call call, List<String> cannedTextResponses) {
LogUtil.v(
"TelecomCallCallback.onStateChanged",
"call=" + call + " cannedTextResponses=" + cannedTextResponses);
for (CannedTextResponsesLoadedListener listener : mCannedTextResponsesLoadedListeners) {
listener.onCannedTextResponsesLoaded(DialerCall.this);
}
}
@Override
public void onPostDialWait(Call call, String remainingPostDialSequence) {
LogUtil.v(
"TelecomCallCallback.onStateChanged",
"call=" + call + " remainingPostDialSequence=" + remainingPostDialSequence);
update();
}
@Override
public void onVideoCallChanged(Call call, VideoCall videoCall) {
LogUtil.v(
"TelecomCallCallback.onStateChanged", "call=" + call + " videoCall=" + videoCall);
update();
}
@Override
public void onCallDestroyed(Call call) {
LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call);
call.unregisterCallback(this);
}
@Override
public void onConferenceableCallsChanged(Call call, List<Call> conferenceableCalls) {
LogUtil.v(
"DialerCall.onConferenceableCallsChanged",
"call %s, conferenceable calls: %d",
call,
conferenceableCalls.size());
update();
}
@Override
public void onConnectionEvent(android.telecom.Call call, String event, Bundle extras) {
LogUtil.v(
"DialerCall.onConnectionEvent",
"Call: " + call + ", Event: " + event + ", Extras: " + extras);
switch (event) {
// The Previous attempt to Merge two calls together has failed in Telecom. We must
// now update the UI to possibly re-enable the Merge button based on the number of
// currently conferenceable calls available or Connection Capabilities.
case android.telecom.Connection.EVENT_CALL_MERGE_FAILED:
update();
break;
case TelephonyManagerCompat.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE:
notifyWiFiToLteHandover();
break;
case TelephonyManagerCompat.EVENT_HANDOVER_TO_WIFI_FAILED:
notifyHandoverToWifiFailed();
break;
case TelephonyManagerCompat.EVENT_CALL_REMOTELY_HELD:
isRemotelyHeld = true;
update();
break;
case TelephonyManagerCompat.EVENT_CALL_REMOTELY_UNHELD:
isRemotelyHeld = false;
update();
break;
default:
break;
}
}
};
private long mTimeAddedMs;
public DialerCall(
Context context,
DialerCallDelegate dialerCallDelegate,
Call telecomCall,
LatencyReport latencyReport,
boolean registerCallback) {
Assert.isNotNull(context);
mContext = context;
mDialerCallDelegate = dialerCallDelegate;
mTelecomCall = telecomCall;
mLatencyReport = latencyReport;
mId = ID_PREFIX + Integer.toString(sIdCounter++);
updateFromTelecomCall(registerCallback);
if (registerCallback) {
mTelecomCall.registerCallback(mTelecomCallCallback);
}
mTimeAddedMs = System.currentTimeMillis();
parseCallSpecificAppData();
}
private static int translateState(int state) {
switch (state) {
case Call.STATE_NEW:
case Call.STATE_CONNECTING:
return DialerCall.State.CONNECTING;
case Call.STATE_SELECT_PHONE_ACCOUNT:
return DialerCall.State.SELECT_PHONE_ACCOUNT;
case Call.STATE_DIALING:
return DialerCall.State.DIALING;
case Call.STATE_PULLING_CALL:
return DialerCall.State.PULLING;
case Call.STATE_RINGING:
return DialerCall.State.INCOMING;
case Call.STATE_ACTIVE:
return DialerCall.State.ACTIVE;
case Call.STATE_HOLDING:
return DialerCall.State.ONHOLD;
case Call.STATE_DISCONNECTED:
return DialerCall.State.DISCONNECTED;
case Call.STATE_DISCONNECTING:
return DialerCall.State.DISCONNECTING;
default:
return DialerCall.State.INVALID;
}
}
public static boolean areSame(DialerCall call1, DialerCall call2) {
if (call1 == null && call2 == null) {
return true;
} else if (call1 == null || call2 == null) {
return false;
}
// otherwise compare call Ids
return call1.getId().equals(call2.getId());
}
public static boolean areSameNumber(DialerCall call1, DialerCall call2) {
if (call1 == null && call2 == null) {
return true;
} else if (call1 == null || call2 == null) {
return false;
}
// otherwise compare call Numbers
return TextUtils.equals(call1.getNumber(), call2.getNumber());
}
public void addListener(DialerCallListener listener) {
Assert.isMainThread();
mListeners.add(listener);
}
public void removeListener(DialerCallListener listener) {
Assert.isMainThread();
mListeners.remove(listener);
}
public void addCannedTextResponsesLoadedListener(CannedTextResponsesLoadedListener listener) {
Assert.isMainThread();
mCannedTextResponsesLoadedListeners.add(listener);
}
public void removeCannedTextResponsesLoadedListener(CannedTextResponsesLoadedListener listener) {
Assert.isMainThread();
mCannedTextResponsesLoadedListeners.remove(listener);
}
public void notifyWiFiToLteHandover() {
LogUtil.i("DialerCall.notifyWiFiToLteHandover", "");
for (DialerCallListener listener : mListeners) {
listener.onWiFiToLteHandover();
}
}
public void notifyHandoverToWifiFailed() {
LogUtil.i("DialerCall.notifyHandoverToWifiFailed", "");
for (DialerCallListener listener : mListeners) {
listener.onHandoverToWifiFailure();
}
}
/* package-private */ Call getTelecomCall() {
return mTelecomCall;
}
public StatusHints getStatusHints() {
return mTelecomCall.getDetails().getStatusHints();
}
/**
* @return video settings of the call, null if the call is not a video call.
* @see VideoProfile
*/
public VideoSettings getVideoSettings() {
return mVideoSettings;
}
private void update() {
Trace.beginSection("Update");
int oldState = getState();
// We want to potentially register a video call callback here.
updateFromTelecomCall(true /* registerCallback */);
if (oldState != getState() && getState() == DialerCall.State.DISCONNECTED) {
for (DialerCallListener listener : mListeners) {
listener.onDialerCallDisconnect();
}
} else {
for (DialerCallListener listener : mListeners) {
listener.onDialerCallUpdate();
}
}
Trace.endSection();
}
private void updateFromTelecomCall(boolean registerCallback) {
LogUtil.v("DialerCall.updateFromTelecomCall", mTelecomCall.toString());
final int translatedState = translateState(mTelecomCall.getState());
if (mState != State.BLOCKED) {
setState(translatedState);
setDisconnectCause(mTelecomCall.getDetails().getDisconnectCause());
maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState());
}
if (registerCallback && mTelecomCall.getVideoCall() != null) {
if (mVideoCallCallback == null) {
mVideoCallCallback = new InCallVideoCallCallback(this);
}
mTelecomCall.getVideoCall().registerCallback(mVideoCallCallback);
mIsVideoCallCallbackRegistered = true;
}
mChildCallIds.clear();
final int numChildCalls = mTelecomCall.getChildren().size();
for (int i = 0; i < numChildCalls; i++) {
mChildCallIds.add(
mDialerCallDelegate
.getDialerCallFromTelecomCall(mTelecomCall.getChildren().get(i))
.getId());
}
// The number of conferenced calls can change over the course of the call, so use the
// maximum number of conferenced child calls as the metric for conference call usage.
mLogState.conferencedCalls = Math.max(numChildCalls, mLogState.conferencedCalls);
updateFromCallExtras(mTelecomCall.getDetails().getExtras());
// If the handle of the call has changed, update state for the call determining if it is an
// emergency call.
Uri newHandle = mTelecomCall.getDetails().getHandle();
if (!Objects.equals(mHandle, newHandle)) {
mHandle = newHandle;
updateEmergencyCallState();
}
// If the phone account handle of the call is set, cache capability bit indicating whether
// the phone account supports call subjects.
PhoneAccountHandle newPhoneAccountHandle = mTelecomCall.getDetails().getAccountHandle();
if (!Objects.equals(mPhoneAccountHandle, newPhoneAccountHandle)) {
mPhoneAccountHandle = newPhoneAccountHandle;
if (mPhoneAccountHandle != null) {
PhoneAccount phoneAccount =
mContext.getSystemService(TelecomManager.class).getPhoneAccount(mPhoneAccountHandle);
if (phoneAccount != null) {
mIsCallSubjectSupported =
phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT);
}
}
}
if (mSessionModificationState
== DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
&& isVideoCall()) {
// We find out in {@link InCallVideoCallCallback.onSessionModifyResponseReceived}
// whether the video upgrade request was accepted. We don't clear the session modification
// state right away though to avoid having the UI switch from video to voice to video.
// Once the underlying telecom call updates to video mode it's safe to clear the state.
LogUtil.i(
"DialerCall.updateFromTelecomCall",
"upgraded to video, clearing session modification state");
setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
}
}
/**
* Tests corruption of the {@code callExtras} bundle by calling {@link
* Bundle#containsKey(String)}. If the bundle is corrupted a {@link IllegalArgumentException} will
* be thrown and caught by this function.
*
* @param callExtras the bundle to verify
* @return {@code true} if the bundle is corrupted, {@code false} otherwise.
*/
protected boolean areCallExtrasCorrupted(Bundle callExtras) {
/**
* There's currently a bug in Telephony service (b/25613098) that could corrupt the extras
* bundle, resulting in a IllegalArgumentException while validating data under {@link
* Bundle#containsKey(String)}.
*/
try {
callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS);
return false;
} catch (IllegalArgumentException e) {
LogUtil.e(
"DialerCall.areCallExtrasCorrupted", "callExtras is corrupted, ignoring exception", e);
return true;
}
}
protected void updateFromCallExtras(Bundle callExtras) {
if (callExtras == null || areCallExtrasCorrupted(callExtras)) {
/**
* If the bundle is corrupted, abandon information update as a work around. These are not
* critical for the dialer to function.
*/
return;
}
// Check for a change in the child address and notify any listeners.
if (callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS)) {
String childNumber = callExtras.getString(Connection.EXTRA_CHILD_ADDRESS);
if (!Objects.equals(childNumber, mChildNumber)) {
mChildNumber = childNumber;
for (DialerCallListener listener : mListeners) {
listener.onDialerCallChildNumberChange();
}
}
}
// Last forwarded number comes in as an array of strings. We want to choose the
// last item in the array. The forwarding numbers arrive independently of when the
// call is originally set up, so we need to notify the the UI of the change.
if (callExtras.containsKey(Connection.EXTRA_LAST_FORWARDED_NUMBER)) {
ArrayList<String> lastForwardedNumbers =
callExtras.getStringArrayList(Connection.EXTRA_LAST_FORWARDED_NUMBER);
if (lastForwardedNumbers != null) {
String lastForwardedNumber = null;
if (!lastForwardedNumbers.isEmpty()) {
lastForwardedNumber = lastForwardedNumbers.get(lastForwardedNumbers.size() - 1);
}
if (!Objects.equals(lastForwardedNumber, mLastForwardedNumber)) {
mLastForwardedNumber = lastForwardedNumber;
for (DialerCallListener listener : mListeners) {
listener.onDialerCallLastForwardedNumberChange();
}
}
}
}
// DialerCall subject is present in the extras at the start of call, so we do not need to
// notify any other listeners of this.
if (callExtras.containsKey(Connection.EXTRA_CALL_SUBJECT)) {
String callSubject = callExtras.getString(Connection.EXTRA_CALL_SUBJECT);
if (!Objects.equals(mCallSubject, callSubject)) {
mCallSubject = callSubject;
}
}
}
/**
* Determines if a received upgrade to video request should be cancelled. This can happen if
* another InCall UI responds to the upgrade to video request.
*
* @param newVideoState The new video state.
*/
private void maybeCancelVideoUpgrade(int newVideoState) {
boolean isVideoStateChanged = mVideoState != newVideoState;
if (mSessionModificationState
== DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
&& isVideoStateChanged) {
LogUtil.i("DialerCall.maybeCancelVideoUpgrade", "cancelling upgrade notification");
setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
}
mVideoState = newVideoState;
}
public String getId() {
return mId;
}
public boolean hasShownWiFiToLteHandoverToast() {
return hasShownWiFiToLteHandoverToast;
}
public void setHasShownWiFiToLteHandoverToast() {
hasShownWiFiToLteHandoverToast = true;
}
public boolean showWifiHandoverAlertAsToast() {
return doNotShowDialogForHandoffToWifiFailure;
}
public void setDoNotShowDialogForHandoffToWifiFailure(boolean bool) {
doNotShowDialogForHandoffToWifiFailure = bool;
}
public long getTimeAddedMs() {
return mTimeAddedMs;
}
@Nullable
public String getNumber() {
return TelecomCallUtil.getNumber(mTelecomCall);
}
public void blockCall() {
mTelecomCall.reject(false, null);
setState(State.BLOCKED);
}
@Nullable
public Uri getHandle() {
return mTelecomCall == null ? null : mTelecomCall.getDetails().getHandle();
}
public boolean isEmergencyCall() {
return mIsEmergencyCall;
}
public boolean isPotentialEmergencyCallback() {
// The property PROPERTY_EMERGENCY_CALLBACK_MODE is only set for CDMA calls when the system
// is actually in emergency callback mode (ie data is disabled).
if (hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE)) {
return true;
}
// We want to treat any incoming call that arrives a short time after an outgoing emergency call
// as a potential emergency callback.
if (getExtras() != null
&& getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0)
> 0) {
long lastEmergencyCallMillis =
getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0);
if (isInEmergencyCallbackWindow(lastEmergencyCallMillis)) {
return true;
}
}
return false;
}
boolean isInEmergencyCallbackWindow(long timestampMillis) {
long emergencyCallbackWindowMillis =
ConfigProviderBindings.get(mContext)
.getLong(CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS, TimeUnit.MINUTES.toMillis(5));
return System.currentTimeMillis() - timestampMillis < emergencyCallbackWindowMillis;
}
public int getState() {
if (mTelecomCall != null && mTelecomCall.getParent() != null) {
return State.CONFERENCED;
} else {
return mState;
}
}
public void setState(int state) {
mState = state;
if (mState == State.INCOMING) {
mLogState.isIncoming = true;
} else if (mState == State.DISCONNECTED) {
mLogState.duration =
getConnectTimeMillis() == 0 ? 0 : System.currentTimeMillis() - getConnectTimeMillis();
}
}
public int getNumberPresentation() {
return mTelecomCall == null ? -1 : mTelecomCall.getDetails().getHandlePresentation();
}
public int getCnapNamePresentation() {
return mTelecomCall == null ? -1 : mTelecomCall.getDetails().getCallerDisplayNamePresentation();
}
@Nullable
public String getCnapName() {
return mTelecomCall == null ? null : getTelecomCall().getDetails().getCallerDisplayName();
}
public Bundle getIntentExtras() {
return mTelecomCall.getDetails().getIntentExtras();
}
@Nullable
public Bundle getExtras() {
return mTelecomCall == null ? null : mTelecomCall.getDetails().getExtras();
}
/** @return The child number for the call, or {@code null} if none specified. */
public String getChildNumber() {
return mChildNumber;
}
/** @return The last forwarded number for the call, or {@code null} if none specified. */
public String getLastForwardedNumber() {
return mLastForwardedNumber;
}
/** @return The call subject, or {@code null} if none specified. */
public String getCallSubject() {
return mCallSubject;
}
/**
* @return {@code true} if the call's phone account supports call subjects, {@code false}
* otherwise.
*/
public boolean isCallSubjectSupported() {
return mIsCallSubjectSupported;
}
/** Returns call disconnect cause, defined by {@link DisconnectCause}. */
public DisconnectCause getDisconnectCause() {
if (mState == State.DISCONNECTED || mState == State.IDLE) {
return mDisconnectCause;
}
return new DisconnectCause(DisconnectCause.UNKNOWN);
}
public void setDisconnectCause(DisconnectCause disconnectCause) {
mDisconnectCause = disconnectCause;
mLogState.disconnectCause = mDisconnectCause;
}
/** Returns the possible text message responses. */
public List<String> getCannedSmsResponses() {
return mTelecomCall.getCannedTextResponses();
}
/** Checks if the call supports the given set of capabilities supplied as a bit mask. */
public boolean can(int capabilities) {
int supportedCapabilities = mTelecomCall.getDetails().getCallCapabilities();
if ((capabilities & Call.Details.CAPABILITY_MERGE_CONFERENCE) != 0) {
// We allow you to merge if the capabilities allow it or if it is a call with
// conferenceable calls.
if (mTelecomCall.getConferenceableCalls().isEmpty()
&& ((Call.Details.CAPABILITY_MERGE_CONFERENCE & supportedCapabilities) == 0)) {
// Cannot merge calls if there are no calls to merge with.
return false;
}
capabilities &= ~Call.Details.CAPABILITY_MERGE_CONFERENCE;
}
return (capabilities == (capabilities & supportedCapabilities));
}
public boolean hasProperty(int property) {
return mTelecomCall.getDetails().hasProperty(property);
}
public String getUniqueCallId() {
return uniqueCallId;
}
/** Gets the time when the call first became active. */
public long getConnectTimeMillis() {
return mTelecomCall.getDetails().getConnectTimeMillis();
}
public boolean isConferenceCall() {
return hasProperty(Call.Details.PROPERTY_CONFERENCE);
}
@Nullable
public GatewayInfo getGatewayInfo() {
return mTelecomCall == null ? null : mTelecomCall.getDetails().getGatewayInfo();
}
@Nullable
public PhoneAccountHandle getAccountHandle() {
return mTelecomCall == null ? null : mTelecomCall.getDetails().getAccountHandle();
}
/**
* @return The {@link VideoCall} instance associated with the {@link Call}. Will return {@code
* null} until {@link #updateFromTelecomCall(boolean)} has registered a valid callback on the
* {@link VideoCall}.
*/
public VideoCall getVideoCall() {
return mTelecomCall == null || !mIsVideoCallCallbackRegistered
? null
: mTelecomCall.getVideoCall();
}
public List<String> getChildCallIds() {
return mChildCallIds;
}
public String getParentId() {
Call parentCall = mTelecomCall.getParent();
if (parentCall != null) {
return mDialerCallDelegate.getDialerCallFromTelecomCall(parentCall).getId();
}
return null;
}
public int getVideoState() {
return mTelecomCall.getDetails().getVideoState();
}
public boolean isVideoCall() {
return CallUtil.isVideoEnabled(mContext) && VideoUtils.isVideoCall(getVideoState());
}
/**
* Determines if the call handle is an emergency number or not and caches the result to avoid
* repeated calls to isEmergencyNumber.
*/
private void updateEmergencyCallState() {
mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall);
}
/**
* Gets the video state which was requested via a session modification request.
*
* @return The video state.
*/
public int getRequestedVideoState() {
return mRequestedVideoState;
}
/**
* Handles incoming session modification requests. Stores the pending video request and sets the
* session modification state to {@link
* DialerCall#SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST} so that we can keep
* track of the fact the request was received. Only upgrade requests require user confirmation and
* will be handled by this method. The remote user can turn off their own camera without
* confirmation.
*
* @param videoState The requested video state.
*/
public void setRequestedVideoState(int videoState) {
LogUtil.v("DialerCall.setRequestedVideoState", "videoState: " + videoState);
if (videoState == getVideoState()) {
LogUtil.e("DialerCall.setRequestedVideoState", "clearing session modification state");
setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
return;
}
mRequestedVideoState = videoState;
setSessionModificationState(
DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
for (DialerCallListener listener : mListeners) {
listener.onDialerCallUpgradeToVideo();
}
LogUtil.i(
"DialerCall.setRequestedVideoState",
"mSessionModificationState: %d, videoState: %d",
mSessionModificationState,
videoState);
update();
}
/**
* Gets the current video session modification state.
*
* @return The session modification state.
*/
@SessionModificationState
public int getSessionModificationState() {
return mSessionModificationState;
}
/**
* Set the session modification state. Used to keep track of pending video session modification
* operations and to inform listeners of these changes.
*
* @param state the new session modification state.
*/
public void setSessionModificationState(@SessionModificationState int state) {
boolean hasChanged = mSessionModificationState != state;
if (hasChanged) {
LogUtil.i(
"DialerCall.setSessionModificationState", "%d -> %d", mSessionModificationState, state);
mSessionModificationState = state;
for (DialerCallListener listener : mListeners) {
listener.onDialerCallSessionModificationStateChange(state);
}
}
}
public LogState getLogState() {
return mLogState;
}
/**
* Determines if the call is an external call.
*
* <p>An external call is one which does not exist locally for the {@link
* android.telecom.ConnectionService} it is associated with.
*
* <p>External calls are only supported in N and higher.
*
* @return {@code true} if the call is an external call, {@code false} otherwise.
*/
public boolean isExternalCall() {
return VERSION.SDK_INT >= VERSION_CODES.N
&& hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL);
}
/**
* Determines if the external call is pullable.
*
* <p>An external call is one which does not exist locally for the {@link
* android.telecom.ConnectionService} it is associated with. An external call may be "pullable",
* which means that the user can request it be transferred to the current device.
*
* <p>External calls are only supported in N and higher.
*
* @return {@code true} if the call is an external call, {@code false} otherwise.
*/
public boolean isPullableExternalCall() {
return VERSION.SDK_INT >= VERSION_CODES.N
&& (mTelecomCall.getDetails().getCallCapabilities()
& CallCompat.Details.CAPABILITY_CAN_PULL_CALL)
== CallCompat.Details.CAPABILITY_CAN_PULL_CALL;
}
/**
* Determines if answering this call will cause an ongoing video call to be dropped.
*
* @return {@code true} if answering this call will drop an ongoing video call, {@code false}
* otherwise.
*/
public boolean answeringDisconnectsForegroundVideoCall() {
Bundle extras = getExtras();
if (extras == null
|| !extras.containsKey(CallCompat.Details.EXTRA_ANSWERING_DROPS_FOREGROUND_CALL)) {
return false;
}
return extras.getBoolean(CallCompat.Details.EXTRA_ANSWERING_DROPS_FOREGROUND_CALL);
}
private void parseCallSpecificAppData() {
if (isExternalCall()) {
return;
}
mLogState.callSpecificAppData = CallIntentParser.getCallSpecificAppData(getIntentExtras());
if (mLogState.callSpecificAppData == null) {
mLogState.callSpecificAppData = new CallSpecificAppData();
mLogState.callSpecificAppData.callInitiationType =
CallInitiationType.Type.EXTERNAL_INITIATION;
}
if (getState() == State.INCOMING) {
mLogState.callSpecificAppData.callInitiationType =
CallInitiationType.Type.INCOMING_INITIATION;
}
}
@Override
public String toString() {
if (mTelecomCall == null) {
// This should happen only in testing since otherwise we would never have a null
// Telecom call.
return String.valueOf(mId);
}
return String.format(
Locale.US,
"[%s, %s, %s, %s, children:%s, parent:%s, "
+ "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]",
mId,
State.toString(getState()),
Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()),
Details.propertiesToString(mTelecomCall.getDetails().getCallProperties()),
mChildCallIds,
getParentId(),
this.mTelecomCall.getConferenceableCalls(),
VideoProfile.videoStateToString(mTelecomCall.getDetails().getVideoState()),
mSessionModificationState,
getVideoSettings());
}
public String toSimpleString() {
return super.toString();
}
@CallHistoryStatus
public int getCallHistoryStatus() {
return mCallHistoryStatus;
}
public void setCallHistoryStatus(@CallHistoryStatus int callHistoryStatus) {
mCallHistoryStatus = callHistoryStatus;
}
public boolean didShowCameraPermission() {
return didShowCameraPermission;
}
public void setDidShowCameraPermission(boolean didShow) {
didShowCameraPermission = didShow;
}
public boolean isInGlobalSpamList() {
return isInGlobalSpamList;
}
public void setIsInGlobalSpamList(boolean inSpamList) {
isInGlobalSpamList = inSpamList;
}
public boolean isInUserSpamList() {
return isInUserSpamList;
}
public void setIsInUserSpamList(boolean inSpamList) {
isInUserSpamList = inSpamList;
}
public boolean isInUserWhiteList() {
return isInUserWhiteList;
}
public void setIsInUserWhiteList(boolean inWhiteList) {
isInUserWhiteList = inWhiteList;
}
public boolean isSpam() {
return mIsSpam;
}
public void setSpam(boolean isSpam) {
mIsSpam = isSpam;
}
public boolean isBlocked() {
return mIsBlocked;
}
public void setBlockedStatus(boolean isBlocked) {
mIsBlocked = isBlocked;
}
public boolean isRemotelyHeld() {
return isRemotelyHeld;
}
public boolean isIncoming() {
return mLogState.isIncoming;
}
public LatencyReport getLatencyReport() {
return mLatencyReport;
}
public void unregisterCallback() {
mTelecomCall.unregisterCallback(mTelecomCallCallback);
}
public void acceptUpgradeRequest(int videoState) {
LogUtil.i("DialerCall.acceptUpgradeRequest", "videoState: " + videoState);
VideoProfile videoProfile = new VideoProfile(videoState);
getVideoCall().sendSessionModifyResponse(videoProfile);
setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
}
public void declineUpgradeRequest() {
LogUtil.i("DialerCall.declineUpgradeRequest", "");
VideoProfile videoProfile = new VideoProfile(getVideoState());
getVideoCall().sendSessionModifyResponse(videoProfile);
setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
}
public void phoneAccountSelected(PhoneAccountHandle accountHandle, boolean setDefault) {
LogUtil.i(
"DialerCall.phoneAccountSelected",
"accountHandle: %s, setDefault: %b",
accountHandle,
setDefault);
mTelecomCall.phoneAccountSelected(accountHandle, setDefault);
}
public void disconnect() {
LogUtil.i("DialerCall.disconnect", "");
setState(DialerCall.State.DISCONNECTING);
for (DialerCallListener listener : mListeners) {
listener.onDialerCallUpdate();
}
mTelecomCall.disconnect();
}
public void hold() {
LogUtil.i("DialerCall.hold", "");
mTelecomCall.hold();
}
public void unhold() {
LogUtil.i("DialerCall.unhold", "");
mTelecomCall.unhold();
}
public void splitFromConference() {
LogUtil.i("DialerCall.splitFromConference", "");
mTelecomCall.splitFromConference();
}
public void answer(int videoState) {
LogUtil.i("DialerCall.answer", "videoState: " + videoState);
mTelecomCall.answer(videoState);
}
public void reject(boolean rejectWithMessage, String message) {
LogUtil.i("DialerCall.reject", "");
mTelecomCall.reject(rejectWithMessage, message);
}
/** Return the string label to represent the call provider */
public String getCallProviderLabel() {
if (callProviderLabel == null) {
PhoneAccount account = getPhoneAccount();
if (account != null && !TextUtils.isEmpty(account.getLabel())) {
List<PhoneAccountHandle> accounts =
mContext.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts();
if (accounts != null && accounts.size() > 1) {
callProviderLabel = account.getLabel().toString();
}
}
if (callProviderLabel == null) {
callProviderLabel = "";
}
}
return callProviderLabel;
}
private PhoneAccount getPhoneAccount() {
PhoneAccountHandle accountHandle = getAccountHandle();
if (accountHandle == null) {
return null;
}
return mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
}
public String getCallbackNumber() {
if (callbackNumber == null) {
// Show the emergency callback number if either:
// 1. This is an emergency call.
// 2. The phone is in Emergency Callback Mode, which means we should show the callback
// number.
boolean showCallbackNumber = hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE);
if (isEmergencyCall() || showCallbackNumber) {
callbackNumber = getSubscriptionNumber();
} else {
StatusHints statusHints = getTelecomCall().getDetails().getStatusHints();
if (statusHints != null) {
Bundle extras = statusHints.getExtras();
if (extras != null) {
callbackNumber = extras.getString(TelecomManager.EXTRA_CALL_BACK_NUMBER);
}
}
}
String simNumber =
mContext.getSystemService(TelecomManager.class).getLine1Number(getAccountHandle());
if (!showCallbackNumber && PhoneNumberUtils.compare(callbackNumber, simNumber)) {
LogUtil.v(
"DialerCall.getCallbackNumber",
"numbers are the same (and callback number is not being forced to show);"
+ " not showing the callback number");
callbackNumber = "";
}
if (callbackNumber == null) {
callbackNumber = "";
}
}
return callbackNumber;
}
private String getSubscriptionNumber() {
// If it's an emergency call, and they're not populating the callback number,
// then try to fall back to the phone sub info (to hopefully get the SIM's
// number directly from the telephony layer).
PhoneAccountHandle accountHandle = getAccountHandle();
if (accountHandle != null) {
PhoneAccount account =
mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
if (account != null) {
return getNumberFromHandle(account.getSubscriptionAddress());
}
}
return null;
}
/**
* Specifies whether a number is in the call history or not. {@link #CALL_HISTORY_STATUS_UNKNOWN}
* means there is no result.
*/
@IntDef({
CALL_HISTORY_STATUS_UNKNOWN,
CALL_HISTORY_STATUS_PRESENT,
CALL_HISTORY_STATUS_NOT_PRESENT
})
@Retention(RetentionPolicy.SOURCE)
public @interface CallHistoryStatus {}
/* Defines different states of this call */
public static class State {
public static final int INVALID = 0;
public static final int NEW = 1; /* The call is new. */
public static final int IDLE = 2; /* The call is idle. Nothing active */
public static final int ACTIVE = 3; /* There is an active call */
public static final int INCOMING = 4; /* A normal incoming phone call */
public static final int CALL_WAITING = 5; /* Incoming call while another is active */
public static final int DIALING = 6; /* An outgoing call during dial phase */
public static final int REDIALING = 7; /* Subsequent dialing attempt after a failure */
public static final int ONHOLD = 8; /* An active phone call placed on hold */
public static final int DISCONNECTING = 9; /* A call is being ended. */
public static final int DISCONNECTED = 10; /* State after a call disconnects */
public static final int CONFERENCED = 11; /* DialerCall part of a conference call */
public static final int SELECT_PHONE_ACCOUNT = 12; /* Waiting for account selection */
public static final int CONNECTING = 13; /* Waiting for Telecom broadcast to finish */
public static final int BLOCKED = 14; /* The number was found on the block list */
public static final int PULLING = 15; /* An external call being pulled to the device */
public static boolean isConnectingOrConnected(int state) {
switch (state) {
case ACTIVE:
case INCOMING:
case CALL_WAITING:
case CONNECTING:
case DIALING:
case PULLING:
case REDIALING:
case ONHOLD:
case CONFERENCED:
return true;
default:
}
return false;
}
public static boolean isDialing(int state) {
return state == DIALING || state == PULLING || state == REDIALING;
}
public static String toString(int state) {
switch (state) {
case INVALID:
return "INVALID";
case NEW:
return "NEW";
case IDLE:
return "IDLE";
case ACTIVE:
return "ACTIVE";
case INCOMING:
return "INCOMING";
case CALL_WAITING:
return "CALL_WAITING";
case DIALING:
return "DIALING";
case PULLING:
return "PULLING";
case REDIALING:
return "REDIALING";
case ONHOLD:
return "ONHOLD";
case DISCONNECTING:
return "DISCONNECTING";
case DISCONNECTED:
return "DISCONNECTED";
case CONFERENCED:
return "CONFERENCED";
case SELECT_PHONE_ACCOUNT:
return "SELECT_PHONE_ACCOUNT";
case CONNECTING:
return "CONNECTING";
case BLOCKED:
return "BLOCKED";
default:
return "UNKNOWN";
}
}
}
/**
* Defines different states of session modify requests, which are used to upgrade to video, or
* downgrade to audio.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({
SESSION_MODIFICATION_STATE_NO_REQUEST,
SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
SESSION_MODIFICATION_STATE_REQUEST_FAILED,
SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
})
public @interface SessionModificationState {}
public static final int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
public static final int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
public static final int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
public static final int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
public static class VideoSettings {
public static final int CAMERA_DIRECTION_UNKNOWN = -1;
public static final int CAMERA_DIRECTION_FRONT_FACING = CameraCharacteristics.LENS_FACING_FRONT;
public static final int CAMERA_DIRECTION_BACK_FACING = CameraCharacteristics.LENS_FACING_BACK;
private int mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
/**
* Gets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
* state of the call should be used to infer the camera direction.
*
* @see {@link CameraCharacteristics#LENS_FACING_FRONT}
* @see {@link CameraCharacteristics#LENS_FACING_BACK}
*/
public int getCameraDir() {
return mCameraDirection;
}
/**
* Sets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
* state of the call should be used to infer the camera direction.
*
* @see {@link CameraCharacteristics#LENS_FACING_FRONT}
* @see {@link CameraCharacteristics#LENS_FACING_BACK}
*/
public void setCameraDir(int cameraDirection) {
if (cameraDirection == CAMERA_DIRECTION_FRONT_FACING
|| cameraDirection == CAMERA_DIRECTION_BACK_FACING) {
mCameraDirection = cameraDirection;
} else {
mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
}
}
@Override
public String toString() {
return "(CameraDir:" + getCameraDir() + ")";
}
}
/**
* Tracks any state variables that is useful for logging. There is some amount of overlap with
* existing call member variables, but this duplication helps to ensure that none of these logging
* variables will interface with/and affect call logic.
*/
public static class LogState {
public DisconnectCause disconnectCause;
public boolean isIncoming = false;
public int contactLookupResult = ContactLookupResult.Type.UNKNOWN_LOOKUP_RESULT_TYPE;
public CallSpecificAppData callSpecificAppData;
// If this was a conference call, the total number of calls involved in the conference.
public int conferencedCalls = 0;
public long duration = 0;
public boolean isLogged = false;
private static String lookupToString(int lookupType) {
switch (lookupType) {
case ContactLookupResult.Type.LOCAL_CONTACT:
return "Local";
case ContactLookupResult.Type.LOCAL_CACHE:
return "Cache";
case ContactLookupResult.Type.REMOTE:
return "Remote";
case ContactLookupResult.Type.EMERGENCY:
return "Emergency";
case ContactLookupResult.Type.VOICEMAIL:
return "Voicemail";
default:
return "Not found";
}
}
private static String initiationToString(CallSpecificAppData callSpecificAppData) {
if (callSpecificAppData == null) {
return "null";
}
switch (callSpecificAppData.callInitiationType) {
case CallInitiationType.Type.INCOMING_INITIATION:
return "Incoming";
case CallInitiationType.Type.DIALPAD:
return "Dialpad";
case CallInitiationType.Type.SPEED_DIAL:
return "Speed Dial";
case CallInitiationType.Type.REMOTE_DIRECTORY:
return "Remote Directory";
case CallInitiationType.Type.SMART_DIAL:
return "Smart Dial";
case CallInitiationType.Type.REGULAR_SEARCH:
return "Regular Search";
case CallInitiationType.Type.CALL_LOG:
return "DialerCall Log";
case CallInitiationType.Type.CALL_LOG_FILTER:
return "DialerCall Log Filter";
case CallInitiationType.Type.VOICEMAIL_LOG:
return "Voicemail Log";
case CallInitiationType.Type.CALL_DETAILS:
return "DialerCall Details";
case CallInitiationType.Type.QUICK_CONTACTS:
return "Quick Contacts";
case CallInitiationType.Type.EXTERNAL_INITIATION:
return "External";
case CallInitiationType.Type.LAUNCHER_SHORTCUT:
return "Launcher Shortcut";
default:
return "Unknown: " + callSpecificAppData.callInitiationType;
}
}
@Override
public String toString() {
return String.format(
Locale.US,
"["
+ "%s, " // DisconnectCause toString already describes the object type
+ "isIncoming: %s, "
+ "contactLookup: %s, "
+ "callInitiation: %s, "
+ "duration: %s"
+ "]",
disconnectCause,
isIncoming,
lookupToString(contactLookupResult),
initiationToString(callSpecificAppData),
duration);
}
}
/** Called when canned text responses have been loaded. */
public interface CannedTextResponsesLoadedListener {
void onCannedTextResponsesLoaded(DialerCall call);
}
}