blob: 1ad37e01a8c21e5b92f97c9c3a7cd89b9b8bbc3b [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;
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
import android.net.Uri;
import android.os.Bundle;
import android.os.Trace;
import android.support.annotation.IntDef;
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.TelecomManager;
import android.telecom.VideoProfile;
import android.text.TextUtils;
import com.android.contacts.common.CallUtil;
import com.android.contacts.common.compat.CallSdkCompat;
import com.android.contacts.common.compat.CompatUtils;
import com.android.contacts.common.compat.SdkVersionOverride;
import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
import com.android.contacts.common.testing.NeededForTesting;
import com.android.dialer.util.IntentUtil;
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;
/**
* Describes a single call and its state.
*/
@NeededForTesting
public class Call {
/**
* 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 {}
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;
/* 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; /* Call 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 boolean isConnectingOrConnected(int state) {
switch(state) {
case ACTIVE:
case INCOMING:
case CALL_WAITING:
case CONNECTING:
case DIALING:
case REDIALING:
case ONHOLD:
case CONFERENCED:
return true;
default:
}
return false;
}
public static boolean isDialing(int state) {
return state == DIALING || 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 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.
*/
public static class SessionModificationState {
public static final int NO_REQUEST = 0;
public static final int WAITING_FOR_RESPONSE = 1;
public static final int REQUEST_FAILED = 2;
public static final int RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
public static final int UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
public static final int REQUEST_REJECTED = 5;
}
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;
/**
* 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;
}
}
/**
* 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;
}
@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 {
// Contact lookup type constants
// Unknown lookup result (lookup not completed yet?)
public static final int LOOKUP_UNKNOWN = 0;
public static final int LOOKUP_NOT_FOUND = 1;
public static final int LOOKUP_LOCAL_CONTACT = 2;
public static final int LOOKUP_LOCAL_CACHE = 3;
public static final int LOOKUP_REMOTE_CONTACT = 4;
public static final int LOOKUP_EMERGENCY = 5;
public static final int LOOKUP_VOICEMAIL = 6;
// Call initiation type constants
public static final int INITIATION_UNKNOWN = 0;
public static final int INITIATION_INCOMING = 1;
public static final int INITIATION_DIALPAD = 2;
public static final int INITIATION_SPEED_DIAL = 3;
public static final int INITIATION_REMOTE_DIRECTORY = 4;
public static final int INITIATION_SMART_DIAL = 5;
public static final int INITIATION_REGULAR_SEARCH = 6;
public static final int INITIATION_CALL_LOG = 7;
public static final int INITIATION_CALL_LOG_FILTER = 8;
public static final int INITIATION_VOICEMAIL_LOG = 9;
public static final int INITIATION_CALL_DETAILS = 10;
public static final int INITIATION_QUICK_CONTACTS = 11;
public static final int INITIATION_EXTERNAL = 12;
public DisconnectCause disconnectCause;
public boolean isIncoming = false;
public int contactLookupResult = LOOKUP_UNKNOWN;
public int callInitiationMethod = INITIATION_EXTERNAL;
// 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;
@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(callInitiationMethod),
duration);
}
private static String lookupToString(int lookupType) {
switch (lookupType) {
case LOOKUP_LOCAL_CONTACT:
return "Local";
case LOOKUP_LOCAL_CACHE:
return "Cache";
case LOOKUP_REMOTE_CONTACT:
return "Remote";
case LOOKUP_EMERGENCY:
return "Emergency";
case LOOKUP_VOICEMAIL:
return "Voicemail";
default:
return "Not found";
}
}
private static String initiationToString(int initiationType) {
switch (initiationType) {
case INITIATION_INCOMING:
return "Incoming";
case INITIATION_DIALPAD:
return "Dialpad";
case INITIATION_SPEED_DIAL:
return "Speed Dial";
case INITIATION_REMOTE_DIRECTORY:
return "Remote Directory";
case INITIATION_SMART_DIAL:
return "Smart Dial";
case INITIATION_REGULAR_SEARCH:
return "Regular Search";
case INITIATION_CALL_LOG:
return "Call Log";
case INITIATION_CALL_LOG_FILTER:
return "Call Log Filter";
case INITIATION_VOICEMAIL_LOG:
return "Voicemail Log";
case INITIATION_CALL_DETAILS:
return "Call Details";
case INITIATION_QUICK_CONTACTS:
return "Quick Contacts";
default:
return "Unknown";
}
}
}
private static final String ID_PREFIX = Call.class.getSimpleName() + "_";
private static int sIdCounter = 0;
private final android.telecom.Call.Callback mTelecomCallCallback =
new android.telecom.Call.Callback() {
@Override
public void onStateChanged(android.telecom.Call call, int newState) {
Log.d(this, "TelecomCallCallback onStateChanged call=" + call + " newState="
+ newState);
update();
}
@Override
public void onParentChanged(android.telecom.Call call,
android.telecom.Call newParent) {
Log.d(this, "TelecomCallCallback onParentChanged call=" + call + " newParent="
+ newParent);
update();
}
@Override
public void onChildrenChanged(android.telecom.Call call,
List<android.telecom.Call> children) {
update();
}
@Override
public void onDetailsChanged(android.telecom.Call call,
android.telecom.Call.Details details) {
Log.d(this, "TelecomCallCallback onStateChanged call=" + call + " details="
+ details);
update();
}
@Override
public void onCannedTextResponsesLoaded(android.telecom.Call call,
List<String> cannedTextResponses) {
Log.d(this, "TelecomCallCallback onStateChanged call=" + call
+ " cannedTextResponses=" + cannedTextResponses);
update();
}
@Override
public void onPostDialWait(android.telecom.Call call,
String remainingPostDialSequence) {
Log.d(this, "TelecomCallCallback onStateChanged call=" + call
+ " remainingPostDialSequence=" + remainingPostDialSequence);
update();
}
@Override
public void onVideoCallChanged(android.telecom.Call call,
VideoCall videoCall) {
Log.d(this, "TelecomCallCallback onStateChanged call=" + call + " videoCall="
+ videoCall);
update();
}
@Override
public void onCallDestroyed(android.telecom.Call call) {
Log.d(this, "TelecomCallCallback onStateChanged call=" + call);
call.unregisterCallback(this);
}
@Override
public void onConferenceableCallsChanged(android.telecom.Call call,
List<android.telecom.Call> conferenceableCalls) {
update();
}
};
private final android.telecom.Call mTelecomCall;
private final LatencyReport mLatencyReport;
private boolean mIsEmergencyCall;
private Uri mHandle;
private final String mId;
private int mState = State.INVALID;
private DisconnectCause mDisconnectCause;
private int mSessionModificationState;
private final List<String> mChildCallIds = new ArrayList<>();
private final VideoSettings mVideoSettings = new VideoSettings();
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;
/**
* Indicates whether the phone account associated with this call supports specifying a call
* subject.
*/
private boolean mIsCallSubjectSupported;
private long mTimeAddedMs;
private final LogState mLogState = new LogState();
/**
* Used only to create mock calls for testing
*/
@NeededForTesting
Call(int state) {
mTelecomCall = null;
mLatencyReport = new LatencyReport();
mId = ID_PREFIX + Integer.toString(sIdCounter++);
setState(state);
}
/**
* Creates a new instance of a {@link Call}. Registers a callback for
* {@link android.telecom.Call} events.
*/
public Call(android.telecom.Call telecomCall, LatencyReport latencyReport) {
this(telecomCall, latencyReport, true /* registerCallback */);
}
/**
* Creates a new instance of a {@link Call}. Optionally registers a callback for
* {@link android.telecom.Call} events.
*
* Intended for use when creating a {@link Call} instance for use with the
* {@link ContactInfoCache}, where we do not want to register callbacks for the new call.
*/
public Call(android.telecom.Call telecomCall, LatencyReport latencyReport,
boolean registerCallback) {
mTelecomCall = telecomCall;
mLatencyReport = latencyReport;
mId = ID_PREFIX + Integer.toString(sIdCounter++);
updateFromTelecomCall(registerCallback);
if (registerCallback) {
mTelecomCall.registerCallback(mTelecomCallCallback);
}
mTimeAddedMs = System.currentTimeMillis();
}
public android.telecom.Call getTelecomCall() {
return mTelecomCall;
}
/**
* @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() == Call.State.DISCONNECTED) {
CallList.getInstance().onDisconnect(this);
} else {
CallList.getInstance().onUpdate(this);
}
Trace.endSection();
}
private void updateFromTelecomCall(boolean registerCallback) {
Log.d(this, "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(
CallList.getInstance().getCallByTelecomCall(
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) {
TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager();
PhoneAccount phoneAccount =
TelecomManagerCompat.getPhoneAccount(mgr, mPhoneAccountHandle);
if (phoneAccount != null) {
mIsCallSubjectSupported = phoneAccount.hasCapabilities(
PhoneAccount.CAPABILITY_CALL_SUBJECT);
}
}
}
}
/**
* 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
* @returns {@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) {
Log.e(this, "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;
CallList.getInstance().onChildNumberChange(this);
}
}
// 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;
CallList.getInstance().onLastForwardedNumberChange(this);
}
}
}
// Call 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 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST
&& isVideoStateChanged) {
Log.v(this, "maybeCancelVideoUpgrade : cancelling upgrade notification");
setSessionModificationState(SessionModificationState.NO_REQUEST);
}
mVideoState = newVideoState;
}
private static int translateState(int state) {
switch (state) {
case android.telecom.Call.STATE_NEW:
case android.telecom.Call.STATE_CONNECTING:
return Call.State.CONNECTING;
case android.telecom.Call.STATE_SELECT_PHONE_ACCOUNT:
return Call.State.SELECT_PHONE_ACCOUNT;
case android.telecom.Call.STATE_DIALING:
return Call.State.DIALING;
case android.telecom.Call.STATE_RINGING:
return Call.State.INCOMING;
case android.telecom.Call.STATE_ACTIVE:
return Call.State.ACTIVE;
case android.telecom.Call.STATE_HOLDING:
return Call.State.ONHOLD;
case android.telecom.Call.STATE_DISCONNECTED:
return Call.State.DISCONNECTED;
case android.telecom.Call.STATE_DISCONNECTING:
return Call.State.DISCONNECTING;
default:
return Call.State.INVALID;
}
}
public String getId() {
return mId;
}
public long getTimeAddedMs() {
return mTimeAddedMs;
}
public String getNumber() {
return TelecomCallUtil.getNumber(mTelecomCall);
}
public void blockCall() {
mTelecomCall.reject(false, null);
setState(State.BLOCKED);
}
public Uri getHandle() {
return mTelecomCall == null ? null : mTelecomCall.getDetails().getHandle();
}
public boolean isEmergencyCall() {
return mIsEmergencyCall;
}
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 ? null : mTelecomCall.getDetails().getHandlePresentation();
}
public int getCnapNamePresentation() {
return mTelecomCall == null ? null
: mTelecomCall.getDetails().getCallerDisplayNamePresentation();
}
public String getCnapName() {
return mTelecomCall == null ? null
: getTelecomCall().getDetails().getCallerDisplayName();
}
public Bundle getIntentExtras() {
return mTelecomCall.getDetails().getIntentExtras();
}
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 & android.telecom.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() &&
((android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE
& supportedCapabilities) == 0)) {
// Cannot merge calls if there are no calls to merge with.
return false;
}
capabilities &= ~android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE;
}
return (capabilities == (capabilities & mTelecomCall.getDetails().getCallCapabilities()));
}
public boolean hasProperty(int property) {
return mTelecomCall.getDetails().hasProperty(property);
}
/** Gets the time when the call first became active. */
public long getConnectTimeMillis() {
return mTelecomCall.getDetails().getConnectTimeMillis();
}
public boolean isConferenceCall() {
return hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE);
}
public GatewayInfo getGatewayInfo() {
return mTelecomCall == null ? null : mTelecomCall.getDetails().getGatewayInfo();
}
public PhoneAccountHandle getAccountHandle() {
return mTelecomCall == null ? null : mTelecomCall.getDetails().getAccountHandle();
}
/**
* @return The {@link VideoCall} instance associated with the {@link android.telecom.Call}.
* Will return {@code null} until {@link #updateFromTelecomCall()} 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() {
android.telecom.Call parentCall = mTelecomCall.getParent();
if (parentCall != null) {
return CallList.getInstance().getCallByTelecomCall(parentCall).getId();
}
return null;
}
public int getVideoState() {
return mTelecomCall.getDetails().getVideoState();
}
public boolean isVideoCall(Context context) {
return CallUtil.isVideoEnabled(context) &&
VideoUtils.isVideoCall(getVideoState());
}
/**
* Handles incoming session modification requests. Stores the pending video request and sets
* the session modification state to
* {@link SessionModificationState#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) {
Log.d(this, "setRequestedVideoState - video state= " + videoState);
if (videoState == getVideoState()) {
mSessionModificationState = Call.SessionModificationState.NO_REQUEST;
Log.w(this,"setRequestedVideoState - Clearing session modification state");
return;
}
mSessionModificationState = Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
mRequestedVideoState = videoState;
CallList.getInstance().onUpgradeToVideo(this);
Log.d(this, "setRequestedVideoState - mSessionModificationState="
+ mSessionModificationState + " video state= " + videoState);
update();
}
/**
* 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(int state) {
boolean hasChanged = mSessionModificationState != state;
mSessionModificationState = state;
Log.d(this, "setSessionModificationState " + state + " mSessionModificationState="
+ mSessionModificationState);
if (hasChanged) {
CallList.getInstance().onSessionModificationStateChange(this, state);
}
}
/**
* 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;
}
public static boolean areSame(Call call1, Call 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(Call call1, Call 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());
}
/**
* Gets the current video session modification state.
*
* @return The session modification state.
*/
public int getSessionModificationState() {
return mSessionModificationState;
}
public LogState getLogState() {
return mLogState;
}
/**
* Determines if the call is an external call.
*
* An external call is one which does not exist locally for the
* {@link android.telecom.ConnectionService} it is associated with.
*
* 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 CompatUtils.isNCompatible() &&
hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL);
}
/**
* Determines if the external call is pullable.
*
* 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.
*
* 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 CompatUtils.isNCompatible() &&
(mTelecomCall.getDetails().getCallCapabilities()
& CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL)
== CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL;
}
/**
* Logging utility methods
*/
public void logCallInitiationType() {
if (isExternalCall()) {
return;
}
if (getState() == State.INCOMING) {
getLogState().callInitiationMethod = LogState.INITIATION_INCOMING;
} else if (getIntentExtras() != null) {
getLogState().callInitiationMethod =
getIntentExtras().getInt(IntentUtil.EXTRA_CALL_INITIATION_TYPE,
LogState.INITIATION_EXTERNAL);
}
}
@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();
}
public void setCallHistoryStatus(@CallHistoryStatus int callHistoryStatus) {
mCallHistoryStatus = callHistoryStatus;
}
@CallHistoryStatus
public int getCallHistoryStatus() {
return mCallHistoryStatus;
}
public void setSpam(boolean isSpam) {
mIsSpam = isSpam;
}
public boolean isSpam() {
return mIsSpam;
}
public LatencyReport getLatencyReport() {
return mLatencyReport;
}
}