blob: b02894683f3a23d1bf4018cdae34b4fbdd52142a [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.Manifest.permission;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.SystemClock;
import android.os.Trace;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.os.BuildCompat;
import android.telecom.Call;
import android.telecom.Call.Details;
import android.telecom.Call.RttCall;
import android.telecom.CallAudioState;
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.text.TextUtils;
import android.widget.Toast;
import com.android.contacts.common.compat.CallCompat;
import com.android.dialer.assisteddialing.ConcreteCreator;
import com.android.dialer.assisteddialing.TransformationInfo;
import com.android.dialer.blocking.FilteredNumbersUtil;
import com.android.dialer.callintent.CallInitiationType;
import com.android.dialer.callintent.CallIntentParser;
import com.android.dialer.callintent.CallSpecificAppData;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DefaultFutureCallback;
import com.android.dialer.compat.telephony.TelephonyManagerCompat;
import com.android.dialer.configprovider.ConfigProviderComponent;
import com.android.dialer.duo.DuoComponent;
import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
import com.android.dialer.enrichedcall.EnrichedCallComponent;
import com.android.dialer.enrichedcall.EnrichedCallManager;
import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener;
import com.android.dialer.enrichedcall.EnrichedCallManager.Filter;
import com.android.dialer.enrichedcall.EnrichedCallManager.StateChangedListener;
import com.android.dialer.enrichedcall.Session;
import com.android.dialer.location.GeoUtil;
import com.android.dialer.logging.ContactLookupResult;
import com.android.dialer.logging.ContactLookupResult.Type;
import com.android.dialer.logging.DialerImpression;
import com.android.dialer.logging.Logger;
import com.android.dialer.preferredsim.PreferredAccountRecorder;
import com.android.dialer.rtt.RttTranscript;
import com.android.dialer.rtt.RttTranscriptUtil;
import com.android.dialer.spam.status.SpamStatus;
import com.android.dialer.telecom.TelecomCallUtil;
import com.android.dialer.telecom.TelecomUtil;
import com.android.dialer.theme.common.R;
import com.android.dialer.time.Clock;
import com.android.dialer.util.PermissionsUtil;
import com.android.incallui.audiomode.AudioModeProvider;
import com.android.incallui.call.state.DialerCallState;
import com.android.incallui.latencyreport.LatencyReport;
import com.android.incallui.rtt.protocol.RttChatMessage;
import com.android.incallui.videotech.VideoTech;
import com.android.incallui.videotech.VideoTech.VideoTechListener;
import com.android.incallui.videotech.duo.DuoVideoTech;
import com.android.incallui.videotech.empty.EmptyVideoTech;
import com.android.incallui.videotech.ims.ImsVideoTech;
import com.android.incallui.videotech.utils.VideoUtils;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import java.io.IOException;
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 implements VideoTechListener, StateChangedListener, CapabilitiesListener {
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;
// Hard coded property for {@code Call}. Upstreamed change from Motorola.
// TODO(a bug): Move it to Telecom in framework.
public static final int PROPERTY_CODEC_KNOWN = 0x04000000;
private static final String ID_PREFIX = "DialerCall_";
@VisibleForTesting
public static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS =
"emergency_callback_window_millis";
private static int idCounter = 0;
public static final int UNKNOWN_PEER_DIMENSIONS = -1;
/**
* A counter used to append to restricted/private/hidden calls so that users can identify them in
* a conversation. This value is reset in {@link CallList#onCallRemoved(Context, Call)} when there
* are no live calls.
*/
private static int hiddenCounter;
/**
* 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 telecomCall;
private final LatencyReport latencyReport;
private final String id;
private final int hiddenId;
private final List<String> childCallIds = new ArrayList<>();
private final LogState logState = new LogState();
private final Context context;
private final DialerCallDelegate dialerCallDelegate;
private final List<DialerCallListener> listeners = new CopyOnWriteArrayList<>();
private final List<CannedTextResponsesLoadedListener> cannedTextResponsesLoadedListeners =
new CopyOnWriteArrayList<>();
private final VideoTechManager videoTechManager;
private boolean isSpeakEasyCall;
private boolean isEmergencyCall;
private Uri handle;
private int state = DialerCallState.INVALID;
private DisconnectCause disconnectCause;
private boolean hasShownLteToWiFiHandoverToast;
private boolean hasShownWiFiToLteHandoverToast;
private boolean doNotShowDialogForHandoffToWifiFailure;
private String childNumber;
private String lastForwardedNumber;
private boolean isCallForwarded;
private String callSubject;
@Nullable private PhoneAccountHandle phoneAccountHandle;
@CallHistoryStatus private int callHistoryStatus = CALL_HISTORY_STATUS_UNKNOWN;
@Nullable private SpamStatus spamStatus;
private boolean isBlocked;
private boolean isOutgoing;
private boolean didShowCameraPermission;
private boolean didDismissVideoChargesAlertDialog;
private PersistableBundle carrierConfig;
private String callProviderLabel;
private String callbackNumber;
private int cameraDirection = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
private EnrichedCallCapabilities enrichedCallCapabilities;
private Session enrichedCallSession;
private int answerAndReleaseButtonDisplayedTimes = 0;
private boolean releasedByAnsweringSecondCall = false;
// Times when a second call is received but AnswerAndRelease button is not shown
// since it's not supported.
private int secondCallWithoutAnswerAndReleasedButtonTimes = 0;
private VideoTech videoTech;
private com.android.dialer.logging.VideoTech.Type selectedAvailableVideoTechType =
com.android.dialer.logging.VideoTech.Type.NONE;
private boolean isVoicemailNumber;
private List<PhoneAccountHandle> callCapableAccounts;
private String countryIso;
private volatile boolean feedbackRequested = false;
private Clock clock = System::currentTimeMillis;
@Nullable private PreferredAccountRecorder preferredAccountRecorder;
private boolean isCallRemoved;
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
* DialerCallState#ONHOLD} state which indicates that the call is being held locally on the
* device.
*/
private boolean isRemotelyHeld;
/** Indicates whether this call is currently in the process of being merged into a conference. */
private boolean isMergeInProcess;
/**
* Indicates whether the phone account associated with this call supports specifying a call
* subject.
*/
private boolean isCallSubjectSupported;
public RttTranscript getRttTranscript() {
return rttTranscript;
}
public void setRttTranscript(RttTranscript rttTranscript) {
this.rttTranscript = rttTranscript;
}
private RttTranscript rttTranscript;
private final Call.Callback telecomCallCallback =
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.onDetailsChanged", " call=" + call + " details=" + details);
update();
}
@Override
public void onCannedTextResponsesLoaded(Call call, List<String> cannedTextResponses) {
LogUtil.v(
"TelecomCallCallback.onCannedTextResponsesLoaded",
"call=" + call + " cannedTextResponses=" + cannedTextResponses);
for (CannedTextResponsesLoadedListener listener : cannedTextResponsesLoadedListeners) {
listener.onCannedTextResponsesLoaded(DialerCall.this);
}
}
@Override
public void onPostDialWait(Call call, String remainingPostDialSequence) {
LogUtil.v(
"TelecomCallCallback.onPostDialWait",
"call=" + call + " remainingPostDialSequence=" + remainingPostDialSequence);
update();
}
@Override
public void onVideoCallChanged(Call call, VideoCall videoCall) {
LogUtil.v(
"TelecomCallCallback.onVideoCallChanged", "call=" + call + " videoCall=" + videoCall);
update();
}
@Override
public void onCallDestroyed(Call call) {
LogUtil.v("TelecomCallCallback.onCallDestroyed", "call=" + call);
unregisterCallback();
}
@Override
public void onConferenceableCallsChanged(Call call, List<Call> conferenceableCalls) {
LogUtil.v(
"TelecomCallCallback.onConferenceableCallsChanged",
"call %s, conferenceable calls: %d",
call,
conferenceableCalls.size());
update();
}
@Override
public void onRttModeChanged(Call call, int mode) {
LogUtil.v("TelecomCallCallback.onRttModeChanged", "mode=%d", mode);
}
@Override
public void onRttRequest(Call call, int id) {
LogUtil.v("TelecomCallCallback.onRttRequest", "id=%d", id);
for (DialerCallListener listener : listeners) {
listener.onDialerCallUpgradeToRtt(id);
}
}
@Override
public void onRttInitiationFailure(Call call, int reason) {
LogUtil.v("TelecomCallCallback.onRttInitiationFailure", "reason=%d", reason);
Toast.makeText(context, R.string.rtt_call_not_available_toast, Toast.LENGTH_LONG).show();
update();
}
@Override
public void onRttStatusChanged(Call call, boolean enabled, RttCall rttCall) {
LogUtil.v("TelecomCallCallback.onRttStatusChanged", "enabled=%b", enabled);
if (enabled) {
Logger.get(context)
.logCallImpression(
DialerImpression.Type.RTT_MID_CALL_ENABLED,
getUniqueCallId(),
getTimeAddedMs());
}
update();
}
@Override
public void onConnectionEvent(android.telecom.Call call, String event, Bundle extras) {
LogUtil.v(
"TelecomCallCallback.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:
isMergeInProcess = false;
update();
break;
case TelephonyManagerCompat.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE:
notifyWiFiToLteHandover();
break;
case TelephonyManagerCompat.EVENT_HANDOVER_VIDEO_FROM_LTE_TO_WIFI:
onLteToWifiHandover();
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;
case TelephonyManagerCompat.EVENT_NOTIFY_INTERNATIONAL_CALL_ON_WFC:
notifyInternationalCallOnWifi();
break;
case TelephonyManagerCompat.EVENT_MERGE_START:
LogUtil.i("DialerCall.onConnectionEvent", "merge start");
isMergeInProcess = true;
break;
case TelephonyManagerCompat.EVENT_MERGE_COMPLETE:
LogUtil.i("DialerCall.onConnectionEvent", "merge complete");
isMergeInProcess = false;
break;
case TelephonyManagerCompat.EVENT_CALL_FORWARDED:
// Only handle this event for P+ since it's unreliable pre-P.
if (BuildCompat.isAtLeastP()) {
isCallForwarded = true;
update();
}
break;
default:
break;
}
}
};
private long timeAddedMs;
private int peerDimensionWidth = UNKNOWN_PEER_DIMENSIONS;
private int peerDimensionHeight = UNKNOWN_PEER_DIMENSIONS;
public DialerCall(
Context context,
DialerCallDelegate dialerCallDelegate,
Call telecomCall,
LatencyReport latencyReport,
boolean registerCallback) {
Assert.isNotNull(context);
this.context = context;
this.dialerCallDelegate = dialerCallDelegate;
this.telecomCall = telecomCall;
this.latencyReport = latencyReport;
id = ID_PREFIX + Integer.toString(idCounter++);
// Must be after assigning mTelecomCall
videoTechManager = new VideoTechManager(this);
updateFromTelecomCall();
if (isHiddenNumber() && TextUtils.isEmpty(getNumber())) {
hiddenId = ++hiddenCounter;
} else {
hiddenId = 0;
}
if (registerCallback) {
this.telecomCall.registerCallback(telecomCallCallback);
}
timeAddedMs = System.currentTimeMillis();
parseCallSpecificAppData();
updateEnrichedCallSession();
}
private static int translateState(int state) {
switch (state) {
case Call.STATE_NEW:
case Call.STATE_CONNECTING:
return DialerCallState.CONNECTING;
case Call.STATE_SELECT_PHONE_ACCOUNT:
return DialerCallState.SELECT_PHONE_ACCOUNT;
case Call.STATE_DIALING:
return DialerCallState.DIALING;
case Call.STATE_PULLING_CALL:
return DialerCallState.PULLING;
case Call.STATE_RINGING:
return DialerCallState.INCOMING;
case Call.STATE_ACTIVE:
return DialerCallState.ACTIVE;
case Call.STATE_HOLDING:
return DialerCallState.ONHOLD;
case Call.STATE_DISCONNECTED:
return DialerCallState.DISCONNECTED;
case Call.STATE_DISCONNECTING:
return DialerCallState.DISCONNECTING;
default:
return DialerCallState.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 void addListener(DialerCallListener listener) {
Assert.isMainThread();
listeners.add(listener);
}
public void removeListener(DialerCallListener listener) {
Assert.isMainThread();
listeners.remove(listener);
}
public void addCannedTextResponsesLoadedListener(CannedTextResponsesLoadedListener listener) {
Assert.isMainThread();
cannedTextResponsesLoadedListeners.add(listener);
}
public void removeCannedTextResponsesLoadedListener(CannedTextResponsesLoadedListener listener) {
Assert.isMainThread();
cannedTextResponsesLoadedListeners.remove(listener);
}
private void onLteToWifiHandover() {
LogUtil.enterBlock("DialerCall.onLteToWifiHandover");
if (hasShownLteToWiFiHandoverToast) {
return;
}
Toast.makeText(context, R.string.video_call_lte_to_wifi_handover_toast, Toast.LENGTH_LONG)
.show();
hasShownLteToWiFiHandoverToast = true;
}
public void notifyWiFiToLteHandover() {
LogUtil.i("DialerCall.notifyWiFiToLteHandover", "");
for (DialerCallListener listener : listeners) {
listener.onWiFiToLteHandover();
}
}
public void notifyHandoverToWifiFailed() {
LogUtil.i("DialerCall.notifyHandoverToWifiFailed", "");
for (DialerCallListener listener : listeners) {
listener.onHandoverToWifiFailure();
}
}
public void notifyInternationalCallOnWifi() {
LogUtil.enterBlock("DialerCall.notifyInternationalCallOnWifi");
for (DialerCallListener dialerCallListener : listeners) {
dialerCallListener.onInternationalCallOnWifi();
}
}
/* package-private */ Call getTelecomCall() {
return telecomCall;
}
public StatusHints getStatusHints() {
return telecomCall.getDetails().getStatusHints();
}
public int getCameraDir() {
return cameraDirection;
}
public void setCameraDir(int cameraDir) {
if (cameraDir == CameraDirection.CAMERA_DIRECTION_FRONT_FACING
|| cameraDir == CameraDirection.CAMERA_DIRECTION_BACK_FACING) {
cameraDirection = cameraDir;
} else {
cameraDirection = CameraDirection.CAMERA_DIRECTION_UNKNOWN;
}
}
public boolean wasParentCall() {
return logState.conferencedCalls != 0;
}
public boolean isVoiceMailNumber() {
return isVoicemailNumber;
}
public List<PhoneAccountHandle> getCallCapableAccounts() {
return callCapableAccounts;
}
public String getCountryIso() {
return countryIso;
}
private void updateIsVoiceMailNumber() {
if (getHandle() != null && PhoneAccount.SCHEME_VOICEMAIL.equals(getHandle().getScheme())) {
isVoicemailNumber = true;
return;
}
if (!PermissionsUtil.hasPermission(context, permission.READ_PHONE_STATE)) {
isVoicemailNumber = false;
return;
}
isVoicemailNumber = TelecomUtil.isVoicemailNumber(context, getAccountHandle(), getNumber());
}
private void update() {
Trace.beginSection("DialerCall.update");
int oldState = getState();
// Clear any cache here that could potentially change on update.
videoTech = null;
// We want to potentially register a video call callback here.
updateFromTelecomCall();
if (oldState != getState() && getState() == DialerCallState.DISCONNECTED) {
for (DialerCallListener listener : listeners) {
listener.onDialerCallDisconnect();
}
EnrichedCallComponent.get(context)
.getEnrichedCallManager()
.unregisterCapabilitiesListener(this);
EnrichedCallComponent.get(context)
.getEnrichedCallManager()
.unregisterStateChangedListener(this);
} else {
for (DialerCallListener listener : listeners) {
listener.onDialerCallUpdate();
}
}
Trace.endSection();
}
@SuppressWarnings("MissingPermission")
private void updateFromTelecomCall() {
Trace.beginSection("DialerCall.updateFromTelecomCall");
LogUtil.v("DialerCall.updateFromTelecomCall", telecomCall.toString());
videoTechManager.dispatchCallStateChanged(telecomCall.getState(), getAccountHandle());
final int translatedState = translateState(telecomCall.getState());
if (state != DialerCallState.BLOCKED) {
setState(translatedState);
setDisconnectCause(telecomCall.getDetails().getDisconnectCause());
}
childCallIds.clear();
final int numChildCalls = telecomCall.getChildren().size();
for (int i = 0; i < numChildCalls; i++) {
childCallIds.add(
dialerCallDelegate
.getDialerCallFromTelecomCall(telecomCall.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.
logState.conferencedCalls = Math.max(numChildCalls, logState.conferencedCalls);
updateFromCallExtras(telecomCall.getDetails().getExtras());
// If the handle of the call has changed, update state for the call determining if it is an
// emergency call.
Uri newHandle = telecomCall.getDetails().getHandle();
if (!Objects.equals(handle, newHandle)) {
handle = newHandle;
updateEmergencyCallState();
}
TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
// If the phone account handle of the call is set, cache capability bit indicating whether
// the phone account supports call subjects.
PhoneAccountHandle newPhoneAccountHandle = telecomCall.getDetails().getAccountHandle();
if (!Objects.equals(phoneAccountHandle, newPhoneAccountHandle)) {
phoneAccountHandle = newPhoneAccountHandle;
if (phoneAccountHandle != null) {
PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle);
if (phoneAccount != null) {
isCallSubjectSupported =
phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT);
if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
cacheCarrierConfiguration(phoneAccountHandle);
}
}
}
}
if (PermissionsUtil.hasPermission(context, permission.READ_PHONE_STATE)) {
updateIsVoiceMailNumber();
callCapableAccounts = telecomManager.getCallCapablePhoneAccounts();
countryIso = GeoUtil.getCurrentCountryIso(context);
}
Trace.endSection();
}
/**
* Caches frequently used carrier configuration locally.
*
* @param accountHandle The PhoneAccount handle.
*/
@SuppressLint("MissingPermission")
private void cacheCarrierConfiguration(PhoneAccountHandle accountHandle) {
if (!PermissionsUtil.hasPermission(context, permission.READ_PHONE_STATE)) {
return;
}
if (VERSION.SDK_INT < VERSION_CODES.O) {
return;
}
// TODO(a bug): This may take several seconds to complete, revisit it to move it to worker
// thread.
carrierConfig =
TelephonyManagerCompat.getTelephonyManagerForPhoneAccountHandle(context, accountHandle)
.getCarrierConfig();
}
/**
* 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 (a bug) 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, this.childNumber)) {
this.childNumber = childNumber;
for (DialerCallListener listener : listeners) {
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, this.lastForwardedNumber)) {
this.lastForwardedNumber = lastForwardedNumber;
for (DialerCallListener listener : listeners) {
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(this.callSubject, callSubject)) {
this.callSubject = callSubject;
}
}
}
public String getId() {
return id;
}
/**
* @return name appended with a number if the number is restricted/unknown and the user has
* received more than one restricted/unknown call.
*/
@Nullable
public String updateNameIfRestricted(@Nullable String name) {
if (name != null && isHiddenNumber() && hiddenId != 0 && hiddenCounter > 1) {
return context.getString(R.string.unknown_counter, name, hiddenId);
}
return name;
}
public static void clearRestrictedCount() {
hiddenCounter = 0;
}
private boolean isHiddenNumber() {
return getNumberPresentation() == TelecomManager.PRESENTATION_RESTRICTED
|| getNumberPresentation() == TelecomManager.PRESENTATION_UNKNOWN;
}
public boolean hasShownWiFiToLteHandoverToast() {
return hasShownWiFiToLteHandoverToast;
}
public void setHasShownWiFiToLteHandoverToast() {
hasShownWiFiToLteHandoverToast = true;
}
public boolean showWifiHandoverAlertAsToast() {
return doNotShowDialogForHandoffToWifiFailure;
}
public void setDoNotShowDialogForHandoffToWifiFailure(boolean bool) {
doNotShowDialogForHandoffToWifiFailure = bool;
}
public boolean showVideoChargesAlertDialog() {
if (carrierConfig == null) {
return false;
}
return carrierConfig.getBoolean(
TelephonyManagerCompat.CARRIER_CONFIG_KEY_SHOW_VIDEO_CALL_CHARGES_ALERT_DIALOG_BOOL);
}
public long getTimeAddedMs() {
return timeAddedMs;
}
@Nullable
public String getNumber() {
return TelecomCallUtil.getNumber(telecomCall);
}
public void blockCall() {
telecomCall.reject(false, null);
setState(DialerCallState.BLOCKED);
}
@Nullable
public Uri getHandle() {
return telecomCall == null ? null : telecomCall.getDetails().getHandle();
}
public boolean isEmergencyCall() {
return isEmergencyCall;
}
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;
}
// Call.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS is available starting in O
if (VERSION.SDK_INT < VERSION_CODES.O) {
long timestampMillis = FilteredNumbersUtil.getLastEmergencyCallTimeMillis(context);
return isInEmergencyCallbackWindow(timestampMillis);
}
// 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(Call.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0) > 0) {
long lastEmergencyCallMillis =
getExtras().getLong(Call.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0);
if (isInEmergencyCallbackWindow(lastEmergencyCallMillis)) {
return true;
}
}
return false;
}
boolean isInEmergencyCallbackWindow(long timestampMillis) {
long emergencyCallbackWindowMillis =
ConfigProviderComponent.get(context)
.getConfigProvider()
.getLong(CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS, TimeUnit.MINUTES.toMillis(5));
return System.currentTimeMillis() - timestampMillis < emergencyCallbackWindowMillis;
}
public int getState() {
if (telecomCall != null && telecomCall.getParent() != null) {
return DialerCallState.CONFERENCED;
} else {
return state;
}
}
public int getNonConferenceState() {
return state;
}
public void setState(int state) {
if (state == DialerCallState.INCOMING) {
logState.isIncoming = true;
}
updateCallTiming(state);
this.state = state;
}
private void updateCallTiming(int newState) {
if (newState == DialerCallState.ACTIVE) {
if (this.state == DialerCallState.ACTIVE) {
LogUtil.i("DialerCall.updateCallTiming", "state is already active");
return;
}
logState.dialerConnectTimeMillis = clock.currentTimeMillis();
logState.dialerConnectTimeMillisElapsedRealtime = SystemClock.elapsedRealtime();
}
if (newState == DialerCallState.DISCONNECTED) {
long newDuration =
getConnectTimeMillis() == 0 ? 0 : clock.currentTimeMillis() - getConnectTimeMillis();
if (this.state == DialerCallState.DISCONNECTED) {
LogUtil.i(
"DialerCall.setState",
"ignoring state transition from DISCONNECTED to DISCONNECTED."
+ " Duration would have changed from %s to %s",
logState.telecomDurationMillis,
newDuration);
return;
}
logState.telecomDurationMillis = newDuration;
logState.dialerDurationMillis =
logState.dialerConnectTimeMillis == 0
? 0
: clock.currentTimeMillis() - logState.dialerConnectTimeMillis;
logState.dialerDurationMillisElapsedRealtime =
logState.dialerConnectTimeMillisElapsedRealtime == 0
? 0
: SystemClock.elapsedRealtime() - logState.dialerConnectTimeMillisElapsedRealtime;
} else if (state == DialerCallState.DIALING || state == DialerCallState.CONNECTING) {
isOutgoing = true;
}
}
@VisibleForTesting
void setClock(Clock clock) {
this.clock = clock;
}
public boolean isOutgoing() {
return isOutgoing;
}
public int getNumberPresentation() {
return telecomCall == null ? -1 : telecomCall.getDetails().getHandlePresentation();
}
public int getCnapNamePresentation() {
return telecomCall == null ? -1 : telecomCall.getDetails().getCallerDisplayNamePresentation();
}
@Nullable
public String getCnapName() {
return telecomCall == null ? null : getTelecomCall().getDetails().getCallerDisplayName();
}
public Bundle getIntentExtras() {
return telecomCall.getDetails().getIntentExtras();
}
@Nullable
public Bundle getExtras() {
return telecomCall == null ? null : telecomCall.getDetails().getExtras();
}
/** @return The child number for the call, or {@code null} if none specified. */
public String getChildNumber() {
return childNumber;
}
/** @return The last forwarded number for the call, or {@code null} if none specified. */
public String getLastForwardedNumber() {
return lastForwardedNumber;
}
public boolean isCallForwarded() {
return isCallForwarded;
}
/** @return The call subject, or {@code null} if none specified. */
public String getCallSubject() {
return callSubject;
}
/**
* @return {@code true} if the call's phone account supports call subjects, {@code false}
* otherwise.
*/
public boolean isCallSubjectSupported() {
return isCallSubjectSupported;
}
/** Returns call disconnect cause, defined by {@link DisconnectCause}. */
public DisconnectCause getDisconnectCause() {
if (state == DialerCallState.DISCONNECTED || state == DialerCallState.IDLE) {
return disconnectCause;
}
return new DisconnectCause(DisconnectCause.UNKNOWN);
}
public void setDisconnectCause(DisconnectCause disconnectCause) {
this.disconnectCause = disconnectCause;
logState.disconnectCause = this.disconnectCause;
}
/** Returns the possible text message responses. */
public List<String> getCannedSmsResponses() {
return telecomCall.getCannedTextResponses();
}
/** Checks if the call supports the given set of capabilities supplied as a bit mask. */
@TargetApi(28)
public boolean can(int capabilities) {
int supportedCapabilities = telecomCall.getDetails().getCallCapabilities();
if ((capabilities & Call.Details.CAPABILITY_MERGE_CONFERENCE) != 0) {
boolean hasConferenceableCall = false;
// RTT call is not conferenceable, it's a bug (a bug) in Telecom and we work around it
// here before it's fixed in Telecom.
for (Call call : telecomCall.getConferenceableCalls()) {
if (!(BuildCompat.isAtLeastP() && call.isRttActive())) {
hasConferenceableCall = true;
break;
}
}
// We allow you to merge if the capabilities allow it or if it is a call with
// conferenceable calls.
if (!hasConferenceableCall
&& ((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 telecomCall.getDetails().hasProperty(property);
}
@NonNull
public String getUniqueCallId() {
return uniqueCallId;
}
/** Gets the time when the call first became active. */
public long getConnectTimeMillis() {
return telecomCall.getDetails().getConnectTimeMillis();
}
/**
* Gets the time when the call is created (see {@link Details#getCreationTimeMillis()}). This is
* the same time that is logged as the start time in the Call Log (see {@link
* android.provider.CallLog.Calls#DATE}).
*/
@TargetApi(VERSION_CODES.O)
public long getCreationTimeMillis() {
return telecomCall.getDetails().getCreationTimeMillis();
}
public boolean isConferenceCall() {
return hasProperty(Call.Details.PROPERTY_CONFERENCE);
}
@Nullable
public GatewayInfo getGatewayInfo() {
return telecomCall == null ? null : telecomCall.getDetails().getGatewayInfo();
}
@Nullable
public PhoneAccountHandle getAccountHandle() {
return telecomCall == null ? null : telecomCall.getDetails().getAccountHandle();
}
/** @return The {@link VideoCall} instance associated with the {@link Call}. */
public VideoCall getVideoCall() {
return telecomCall == null ? null : telecomCall.getVideoCall();
}
public List<String> getChildCallIds() {
return childCallIds;
}
public String getParentId() {
Call parentCall = telecomCall.getParent();
if (parentCall != null) {
return dialerCallDelegate.getDialerCallFromTelecomCall(parentCall).getId();
}
return null;
}
public int getVideoState() {
return telecomCall.getDetails().getVideoState();
}
public boolean isVideoCall() {
return getVideoTech().isTransmittingOrReceiving() || VideoProfile.isVideo(getVideoState());
}
@TargetApi(28)
public boolean isActiveRttCall() {
if (BuildCompat.isAtLeastP()) {
return getTelecomCall().isRttActive();
} else {
return false;
}
}
@TargetApi(28)
@Nullable
public RttCall getRttCall() {
if (!isActiveRttCall()) {
return null;
}
return getTelecomCall().getRttCall();
}
@TargetApi(28)
public boolean isPhoneAccountRttCapable() {
PhoneAccount phoneAccount = getPhoneAccount();
if (phoneAccount == null) {
return false;
}
if (!phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_RTT)) {
return false;
}
return true;
}
@TargetApi(28)
public boolean canUpgradeToRttCall() {
if (!isPhoneAccountRttCapable()) {
return false;
}
if (isActiveRttCall()) {
return false;
}
if (isVideoCall()) {
return false;
}
if (isConferenceCall()) {
return false;
}
if (CallList.getInstance().hasActiveRttCall()) {
return false;
}
return true;
}
@TargetApi(28)
public void sendRttUpgradeRequest() {
getTelecomCall().sendRttRequest();
}
@TargetApi(28)
public void respondToRttRequest(boolean accept, int rttRequestId) {
Logger.get(context)
.logCallImpression(
accept
? DialerImpression.Type.RTT_MID_CALL_ACCEPTED
: DialerImpression.Type.RTT_MID_CALL_REJECTED,
getUniqueCallId(),
getTimeAddedMs());
getTelecomCall().respondToRttRequest(rttRequestId, accept);
}
@TargetApi(28)
private void saveRttTranscript() {
if (!BuildCompat.isAtLeastP()) {
return;
}
if (getRttCall() != null) {
// Save any remaining text in the buffer that's not shown by UI yet.
// This may happen when the call is switched to background before disconnect.
try {
String messageLeft = getRttCall().readImmediately();
if (!TextUtils.isEmpty(messageLeft)) {
rttTranscript =
RttChatMessage.getRttTranscriptWithNewRemoteMessage(rttTranscript, messageLeft);
}
} catch (IOException e) {
LogUtil.e("DialerCall.saveRttTranscript", "error when reading remaining message", e);
}
}
// Don't save transcript if it's empty.
if (rttTranscript.getMessagesCount() == 0) {
return;
}
Futures.addCallback(
RttTranscriptUtil.saveRttTranscript(context, rttTranscript),
new DefaultFutureCallback<>(),
MoreExecutors.directExecutor());
}
public boolean hasReceivedVideoUpgradeRequest() {
return VideoUtils.hasReceivedVideoUpgradeRequest(getVideoTech().getSessionModificationState());
}
public boolean hasSentVideoUpgradeRequest() {
return VideoUtils.hasSentVideoUpgradeRequest(getVideoTech().getSessionModificationState());
}
public boolean hasSentRttUpgradeRequest() {
return false;
}
/**
* Determines if the call handle is an emergency number or not and caches the result to avoid
* repeated calls to isEmergencyNumber.
*/
private void updateEmergencyCallState() {
isEmergencyCall = TelecomCallUtil.isEmergencyCall(telecomCall);
}
public LogState getLogState() {
return logState;
}
/**
* 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.
*
* @return {@code true} if the call is an external call, {@code false} otherwise.
*/
boolean isExternalCall() {
return hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_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;
}
logState.callSpecificAppData = CallIntentParser.getCallSpecificAppData(getIntentExtras());
if (logState.callSpecificAppData == null) {
logState.callSpecificAppData =
CallSpecificAppData.newBuilder()
.setCallInitiationType(CallInitiationType.Type.EXTERNAL_INITIATION)
.build();
}
if (getState() == DialerCallState.INCOMING) {
logState.callSpecificAppData =
logState
.callSpecificAppData
.toBuilder()
.setCallInitiationType(CallInitiationType.Type.INCOMING_INITIATION)
.build();
}
}
@Override
public String toString() {
if (telecomCall == null) {
// This should happen only in testing since otherwise we would never have a null
// Telecom call.
return String.valueOf(id);
}
return String.format(
Locale.US,
"[%s, %s, %s, %s, children:%s, parent:%s, "
+ "conferenceable:%s, videoState:%s, mSessionModificationState:%d, CameraDir:%s]",
id,
DialerCallState.toString(getState()),
Details.capabilitiesToString(telecomCall.getDetails().getCallCapabilities()),
Details.propertiesToString(telecomCall.getDetails().getCallProperties()),
childCallIds,
getParentId(),
this.telecomCall.getConferenceableCalls(),
VideoProfile.videoStateToString(telecomCall.getDetails().getVideoState()),
getVideoTech().getSessionModificationState(),
getCameraDir());
}
public String toSimpleString() {
return super.toString();
}
@CallHistoryStatus
public int getCallHistoryStatus() {
return callHistoryStatus;
}
public void setCallHistoryStatus(@CallHistoryStatus int callHistoryStatus) {
this.callHistoryStatus = callHistoryStatus;
}
public boolean didShowCameraPermission() {
return didShowCameraPermission;
}
public void setDidShowCameraPermission(boolean didShow) {
didShowCameraPermission = didShow;
}
public boolean didDismissVideoChargesAlertDialog() {
return didDismissVideoChargesAlertDialog;
}
public void setDidDismissVideoChargesAlertDialog(boolean didDismiss) {
didDismissVideoChargesAlertDialog = didDismiss;
}
public void setSpamStatus(@Nullable SpamStatus spamStatus) {
this.spamStatus = spamStatus;
}
public Optional<SpamStatus> getSpamStatus() {
return Optional.fromNullable(spamStatus);
}
public boolean isSpam() {
if (spamStatus == null || !spamStatus.isSpam()) {
return false;
}
if (!isIncoming()) {
return false;
}
if (isPotentialEmergencyCallback()) {
return false;
}
return true;
}
public boolean isBlocked() {
return isBlocked;
}
public void setBlockedStatus(boolean isBlocked) {
this.isBlocked = isBlocked;
}
public boolean isRemotelyHeld() {
return isRemotelyHeld;
}
public boolean isMergeInProcess() {
return isMergeInProcess;
}
public boolean isIncoming() {
return logState.isIncoming;
}
/**
* Try and determine if the call used assisted dialing.
*
* <p>We will not be able to verify a call underwent assisted dialing until the Platform
* implmentation is complete in P+.
*
* @return a boolean indicating assisted dialing may have been performed
*/
public boolean isAssistedDialed() {
if (getIntentExtras() != null) {
// P and below uses the existence of USE_ASSISTED_DIALING to indicate assisted dialing
// was used. The Dialer client is responsible for performing assisted dialing before
// placing the outgoing call.
//
// The existence of the assisted dialing extras indicates that assisted dialing took place.
if (getIntentExtras().getBoolean(TelephonyManagerCompat.USE_ASSISTED_DIALING, false)
&& getAssistedDialingExtras() != null
&& Build.VERSION.SDK_INT <= ConcreteCreator.BUILD_CODE_CEILING) {
return true;
}
}
return false;
}
@Nullable
public TransformationInfo getAssistedDialingExtras() {
if (getIntentExtras() == null) {
return null;
}
if (getIntentExtras().getBundle(TelephonyManagerCompat.ASSISTED_DIALING_EXTRAS) == null) {
return null;
}
// Used in N-OMR1
return TransformationInfo.newInstanceFromBundle(
getIntentExtras().getBundle(TelephonyManagerCompat.ASSISTED_DIALING_EXTRAS));
}
public LatencyReport getLatencyReport() {
return latencyReport;
}
public int getAnswerAndReleaseButtonDisplayedTimes() {
return answerAndReleaseButtonDisplayedTimes;
}
public void increaseAnswerAndReleaseButtonDisplayedTimes() {
answerAndReleaseButtonDisplayedTimes++;
}
public boolean getReleasedByAnsweringSecondCall() {
return releasedByAnsweringSecondCall;
}
public void setReleasedByAnsweringSecondCall(boolean releasedByAnsweringSecondCall) {
this.releasedByAnsweringSecondCall = releasedByAnsweringSecondCall;
}
public int getSecondCallWithoutAnswerAndReleasedButtonTimes() {
return secondCallWithoutAnswerAndReleasedButtonTimes;
}
public void increaseSecondCallWithoutAnswerAndReleasedButtonTimes() {
secondCallWithoutAnswerAndReleasedButtonTimes++;
}
@Nullable
public EnrichedCallCapabilities getEnrichedCallCapabilities() {
return enrichedCallCapabilities;
}
public void setEnrichedCallCapabilities(
@Nullable EnrichedCallCapabilities mEnrichedCallCapabilities) {
this.enrichedCallCapabilities = mEnrichedCallCapabilities;
}
@Nullable
public Session getEnrichedCallSession() {
return enrichedCallSession;
}
public void setEnrichedCallSession(@Nullable Session mEnrichedCallSession) {
this.enrichedCallSession = mEnrichedCallSession;
}
public void unregisterCallback() {
telecomCall.unregisterCallback(telecomCallCallback);
}
public void phoneAccountSelected(PhoneAccountHandle accountHandle, boolean setDefault) {
LogUtil.i(
"DialerCall.phoneAccountSelected",
"accountHandle: %s, setDefault: %b",
accountHandle,
setDefault);
telecomCall.phoneAccountSelected(accountHandle, setDefault);
}
public void disconnect() {
LogUtil.i("DialerCall.disconnect", "");
setState(DialerCallState.DISCONNECTING);
for (DialerCallListener listener : listeners) {
listener.onDialerCallUpdate();
}
telecomCall.disconnect();
}
public void hold() {
LogUtil.i("DialerCall.hold", "");
telecomCall.hold();
}
public void unhold() {
LogUtil.i("DialerCall.unhold", "");
telecomCall.unhold();
}
public void splitFromConference() {
LogUtil.i("DialerCall.splitFromConference", "");
telecomCall.splitFromConference();
}
public void answer(int videoState) {
LogUtil.i("DialerCall.answer", "videoState: " + videoState);
telecomCall.answer(videoState);
}
public void answer() {
answer(telecomCall.getDetails().getVideoState());
}
public void reject(boolean rejectWithMessage, String message) {
LogUtil.i("DialerCall.reject", "");
telecomCall.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())) {
if (callCapableAccounts != null && callCapableAccounts.size() > 1) {
callProviderLabel = account.getLabel().toString();
}
}
if (callProviderLabel == null) {
callProviderLabel = "";
}
}
return callProviderLabel;
}
private PhoneAccount getPhoneAccount() {
PhoneAccountHandle accountHandle = getAccountHandle();
if (accountHandle == null) {
return null;
}
return context.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
}
public VideoTech getVideoTech() {
if (videoTech == null) {
videoTech = videoTechManager.getVideoTech(getAccountHandle());
// Only store the first video tech type found to be available during the life of the call.
if (selectedAvailableVideoTechType == com.android.dialer.logging.VideoTech.Type.NONE) {
// Update the video tech.
selectedAvailableVideoTechType = videoTech.getVideoTechType();
}
}
return videoTech;
}
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 =
context.getSystemService(TelecomManager.class).getLine1Number(getAccountHandle());
}
if (callbackNumber == null) {
callbackNumber = "";
}
}
return callbackNumber;
}
public String getSimCountryIso() {
String simCountryIso =
TelephonyManagerCompat.getTelephonyManagerForPhoneAccountHandle(context, getAccountHandle())
.getSimCountryIso();
if (!TextUtils.isEmpty(simCountryIso)) {
simCountryIso = simCountryIso.toUpperCase(Locale.US);
}
return simCountryIso;
}
@Override
public void onVideoTechStateChanged() {
update();
}
@Override
public void onSessionModificationStateChanged() {
Trace.beginSection("DialerCall.onSessionModificationStateChanged");
for (DialerCallListener listener : listeners) {
listener.onDialerCallSessionModificationStateChange();
}
Trace.endSection();
}
@Override
public void onCameraDimensionsChanged(int width, int height) {
InCallVideoCallCallbackNotifier.getInstance().cameraDimensionsChanged(this, width, height);
}
@Override
public void onPeerDimensionsChanged(int width, int height) {
peerDimensionWidth = width;
peerDimensionHeight = height;
InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(this, width, height);
}
@Override
public void onVideoUpgradeRequestReceived() {
LogUtil.enterBlock("DialerCall.onVideoUpgradeRequestReceived");
for (DialerCallListener listener : listeners) {
listener.onDialerCallUpgradeToVideo();
}
update();
Logger.get(context)
.logCallImpression(
DialerImpression.Type.VIDEO_CALL_REQUEST_RECEIVED, getUniqueCallId(), getTimeAddedMs());
}
@Override
public void onUpgradedToVideo(boolean switchToSpeaker) {
LogUtil.enterBlock("DialerCall.onUpgradedToVideo");
if (!switchToSpeaker) {
return;
}
CallAudioState audioState = AudioModeProvider.getInstance().getAudioState();
if (0 != (CallAudioState.ROUTE_BLUETOOTH & audioState.getSupportedRouteMask())) {
LogUtil.e(
"DialerCall.onUpgradedToVideo",
"toggling speakerphone not allowed when bluetooth supported.");
return;
}
if (audioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
return;
}
TelecomAdapter.getInstance().setAudioRoute(CallAudioState.ROUTE_SPEAKER);
}
@Override
public void onCapabilitiesUpdated() {
if (getNumber() == null) {
return;
}
EnrichedCallCapabilities capabilities =
EnrichedCallComponent.get(context).getEnrichedCallManager().getCapabilities(getNumber());
if (capabilities != null) {
setEnrichedCallCapabilities(capabilities);
update();
}
}
@Override
public void onEnrichedCallStateChanged() {
updateEnrichedCallSession();
}
@Override
public void onImpressionLoggingNeeded(DialerImpression.Type impressionType) {
Logger.get(context).logCallImpression(impressionType, getUniqueCallId(), getTimeAddedMs());
if (impressionType == DialerImpression.Type.LIGHTBRINGER_UPGRADE_REQUESTED) {
if (getLogState().contactLookupResult == Type.NOT_FOUND) {
Logger.get(context)
.logCallImpression(
DialerImpression.Type.LIGHTBRINGER_NON_CONTACT_UPGRADE_REQUESTED,
getUniqueCallId(),
getTimeAddedMs());
}
}
}
private void updateEnrichedCallSession() {
if (getNumber() == null) {
return;
}
if (getEnrichedCallSession() != null) {
// State changes to existing sessions are currently handled by the UI components (which have
// their own listeners). Someday instead we could remove those and just call update() here and
// have the usual onDialerCallUpdate update the UI.
dispatchOnEnrichedCallSessionUpdate();
return;
}
EnrichedCallManager manager = EnrichedCallComponent.get(context).getEnrichedCallManager();
Filter filter =
isIncoming()
? manager.createIncomingCallComposerFilter()
: manager.createOutgoingCallComposerFilter();
Session session = manager.getSession(getUniqueCallId(), getNumber(), filter);
if (session == null) {
return;
}
session.setUniqueDialerCallId(getUniqueCallId());
setEnrichedCallSession(session);
LogUtil.i(
"DialerCall.updateEnrichedCallSession",
"setting session %d's dialer id to %s",
session.getSessionId(),
getUniqueCallId());
dispatchOnEnrichedCallSessionUpdate();
}
private void dispatchOnEnrichedCallSessionUpdate() {
for (DialerCallListener listener : listeners) {
listener.onEnrichedCallSessionUpdate();
}
}
void onRemovedFromCallList() {
LogUtil.enterBlock("DialerCall.onRemovedFromCallList");
// Ensure we clean up when this call is removed.
if (videoTechManager != null) {
videoTechManager.dispatchRemovedFromCallList();
}
// TODO(wangqi): Consider moving this to a DialerCallListener.
if (rttTranscript != null && !isCallRemoved) {
saveRttTranscript();
}
isCallRemoved = true;
}
public com.android.dialer.logging.VideoTech.Type getSelectedAvailableVideoTechType() {
return selectedAvailableVideoTechType;
}
public void markFeedbackRequested() {
feedbackRequested = true;
}
public boolean isFeedbackRequested() {
return feedbackRequested;
}
/**
* If the in call UI has shown the phone account selection dialog for the call, the {@link
* PreferredAccountRecorder} to record the result from the dialog.
*/
@Nullable
public PreferredAccountRecorder getPreferredAccountRecorder() {
return preferredAccountRecorder;
}
public void setPreferredAccountRecorder(PreferredAccountRecorder preferredAccountRecorder) {
this.preferredAccountRecorder = preferredAccountRecorder;
}
/** Indicates the call is eligible for SpeakEasy */
public boolean isSpeakEasyEligible() {
PhoneAccount phoneAccount = getPhoneAccount();
if (phoneAccount == null) {
return false;
}
if (!phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
return false;
}
return !isPotentialEmergencyCallback()
&& !isEmergencyCall()
&& !isActiveRttCall()
&& !isConferenceCall()
&& !isVideoCall()
&& !isVoiceMailNumber()
&& !hasReceivedVideoUpgradeRequest()
&& !isVoipCallNotSupportedBySpeakeasy();
}
private boolean isVoipCallNotSupportedBySpeakeasy() {
Bundle extras = getIntentExtras();
if (extras == null) {
return false;
}
// Indicates an VOIP call.
String callid = extras.getString("callid");
if (TextUtils.isEmpty(callid)) {
LogUtil.i("DialerCall.isVoipCallNotSupportedBySpeakeasy", "callid was empty");
return false;
}
LogUtil.i("DialerCall.isVoipCallNotSupportedBySpeakeasy", "call is not eligible");
return true;
}
/** Indicates the user has selected SpeakEasy */
public boolean isSpeakEasyCall() {
if (!isSpeakEasyEligible()) {
return false;
}
return isSpeakEasyCall;
}
/** Sets the user preference for SpeakEasy */
public void setIsSpeakEasyCall(boolean isSpeakEasyCall) {
this.isSpeakEasyCall = isSpeakEasyCall;
if (listeners != null) {
for (DialerCallListener listener : listeners) {
listener.onDialerCallSpeakEasyStateChange();
}
}
}
/**
* 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 {}
/** Camera direction constants */
public static class CameraDirection {
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;
}
/**
* 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 ContactLookupResult.Type 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 boolean isLogged = false;
// Result of subtracting android.telecom.Call.Details#getConnectTimeMillis from the current time
public long telecomDurationMillis = 0;
// Result of a call to System.currentTimeMillis when Dialer sees that a call
// moves to the ACTIVE state
long dialerConnectTimeMillis = 0;
// Same as dialer_connect_time_millis, using SystemClock.elapsedRealtime
// instead
long dialerConnectTimeMillisElapsedRealtime = 0;
// Result of subtracting dialer_connect_time_millis from System.currentTimeMillis
public long dialerDurationMillis = 0;
// Same as dialerDurationMillis, using SystemClock.elapsedRealtime instead
public long dialerDurationMillisElapsedRealtime = 0;
private static String lookupToString(ContactLookupResult.Type lookupType) {
switch (lookupType) {
case LOCAL_CONTACT:
return "Local";
case LOCAL_CACHE:
return "Cache";
case REMOTE:
return "Remote";
case EMERGENCY:
return "Emergency";
case VOICEMAIL:
return "Voicemail";
default:
return "Not found";
}
}
private static String initiationToString(CallSpecificAppData callSpecificAppData) {
if (callSpecificAppData == null) {
return "null";
}
switch (callSpecificAppData.getCallInitiationType()) {
case INCOMING_INITIATION:
return "Incoming";
case DIALPAD:
return "Dialpad";
case SPEED_DIAL:
return "Speed Dial";
case REMOTE_DIRECTORY:
return "Remote Directory";
case SMART_DIAL:
return "Smart Dial";
case REGULAR_SEARCH:
return "Regular Search";
case CALL_LOG:
return "DialerCall Log";
case CALL_LOG_FILTER:
return "DialerCall Log Filter";
case VOICEMAIL_LOG:
return "Voicemail Log";
case CALL_DETAILS:
return "DialerCall Details";
case QUICK_CONTACTS:
return "Quick Contacts";
case EXTERNAL_INITIATION:
return "External";
case LAUNCHER_SHORTCUT:
return "Launcher Shortcut";
default:
return "Unknown: " + callSpecificAppData.getCallInitiationType();
}
}
@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),
telecomDurationMillis);
}
}
/** Coordinates the available VideoTech implementations for a call. */
@VisibleForTesting
public static class VideoTechManager {
private final Context context;
private final EmptyVideoTech emptyVideoTech = new EmptyVideoTech();
private final VideoTech rcsVideoShare;
private final List<VideoTech> videoTechs;
private VideoTech savedTech;
@VisibleForTesting
public VideoTechManager(DialerCall call) {
this.context = call.context;
String phoneNumber = call.getNumber();
phoneNumber = phoneNumber != null ? phoneNumber : "";
phoneNumber = phoneNumber.replaceAll("[^+0-9]", "");
// Insert order here determines the priority of that video tech option
videoTechs = new ArrayList<>();
videoTechs.add(new ImsVideoTech(Logger.get(call.context), call, call.telecomCall));
rcsVideoShare =
EnrichedCallComponent.get(call.context)
.getRcsVideoShareFactory()
.newRcsVideoShare(
EnrichedCallComponent.get(call.context).getEnrichedCallManager(),
call,
phoneNumber);
videoTechs.add(rcsVideoShare);
videoTechs.add(
new DuoVideoTech(
DuoComponent.get(call.context).getDuo(), call, call.telecomCall, phoneNumber));
savedTech = emptyVideoTech;
}
@VisibleForTesting
public VideoTech getVideoTech(PhoneAccountHandle phoneAccountHandle) {
if (savedTech == emptyVideoTech) {
for (VideoTech tech : videoTechs) {
if (tech.isAvailable(context, phoneAccountHandle)) {
savedTech = tech;
savedTech.becomePrimary();
break;
}
}
} else if (savedTech instanceof DuoVideoTech
&& rcsVideoShare.isAvailable(context, phoneAccountHandle)) {
// RCS Video Share will become available after the capability exchange which is slower than
// Duo reading local contacts for reachability. If Video Share becomes available and we are
// not in the middle of any session changes, let it take over.
savedTech = rcsVideoShare;
rcsVideoShare.becomePrimary();
}
return savedTech;
}
@VisibleForTesting
public void dispatchCallStateChanged(int newState, PhoneAccountHandle phoneAccountHandle) {
for (VideoTech videoTech : videoTechs) {
videoTech.onCallStateChanged(context, newState, phoneAccountHandle);
}
}
void dispatchRemovedFromCallList() {
for (VideoTech videoTech : videoTechs) {
videoTech.onRemovedFromCallList();
}
}
}
/** Called when canned text responses have been loaded. */
public interface CannedTextResponsesLoadedListener {
void onCannedTextResponsesLoaded(DialerCall call);
}
/** Gets peer dimension width. */
public int getPeerDimensionWidth() {
return peerDimensionWidth;
}
/** Gets peer dimension height. */
public int getPeerDimensionHeight() {
return peerDimensionHeight;
}
}