summaryrefslogtreecommitdiff
path: root/java/com/android/incallui/call/DialerCall.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/incallui/call/DialerCall.java')
-rw-r--r--java/com/android/incallui/call/DialerCall.java1401
1 files changed, 1401 insertions, 0 deletions
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
new file mode 100644
index 000000000..bd8f006dd
--- /dev/null
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -0,0 +1,1401 @@
+/*
+ * 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);
+ }
+}