/*
 * 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 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;
    }
  }

  @VisibleForTesting
  void setClock(Clock clock) {
    this.clock = clock;
  }

  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;
  }
}
