content: /*
   * 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.internal.telephony.imsphone;
  
  import static com.android.internal.telephony.Phone.CS_FALLBACK;
  
  import android.annotation.NonNull;
  import android.annotation.UnsupportedAppUsage;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.SharedPreferences;
  import android.content.pm.PackageManager;
  import android.net.ConnectivityManager;
  import android.net.Network;
  import android.net.NetworkCapabilities;
  import android.net.NetworkInfo;
  import android.net.NetworkRequest;
  import android.net.NetworkStats;
  import android.net.Uri;
  import android.os.AsyncResult;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.Message;
  import android.os.PersistableBundle;
  import android.os.Registrant;
  import android.os.RegistrantList;
  import android.os.RemoteException;
  import android.os.SystemClock;
  import android.os.SystemProperties;
  import android.preference.PreferenceManager;
  import android.provider.Settings;
  import android.telecom.ConferenceParticipant;
  import android.telecom.TelecomManager;
  import android.telecom.VideoProfile;
  import android.telephony.CallQuality;
  import android.telephony.CarrierConfigManager;
  import android.telephony.DisconnectCause;
  import android.telephony.PhoneNumberUtils;
  import android.telephony.Rlog;
  import android.telephony.ServiceState;
  import android.telephony.SubscriptionInfo;
  import android.telephony.SubscriptionManager;
  import android.telephony.TelephonyManager;
  import android.telephony.emergency.EmergencyNumber;
  import android.telephony.ims.ImsCallProfile;
  import android.telephony.ims.ImsMmTelManager;
  import android.telephony.ims.ImsReasonInfo;
  import android.telephony.ims.ImsStreamMediaProfile;
  import android.telephony.ims.ImsSuppServiceNotification;
  import android.telephony.ims.ProvisioningManager;
  import android.telephony.ims.feature.ImsFeature;
  import android.telephony.ims.feature.MmTelFeature;
  import android.telephony.ims.stub.ImsRegistrationImplBase;
  import android.text.TextUtils;
  import android.util.ArrayMap;
  import android.util.Log;
  import android.util.Pair;
  
  import com.android.ims.ImsCall;
  import com.android.ims.ImsConfig;
  import com.android.ims.ImsConfigListener;
  import com.android.ims.ImsEcbm;
  import com.android.ims.ImsException;
  import com.android.ims.ImsManager;
  import com.android.ims.ImsMultiEndpoint;
  import com.android.ims.ImsUtInterface;
  import com.android.ims.internal.IImsCallSession;
  import com.android.ims.internal.IImsVideoCallProvider;
  import com.android.ims.internal.ImsVideoCallProviderWrapper;
  import com.android.ims.internal.VideoPauseTracker;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.os.SomeArgs;
  import com.android.internal.telephony.Call;
  import com.android.internal.telephony.CallStateException;
  import com.android.internal.telephony.CallTracker;
  import com.android.internal.telephony.CommandException;
  import com.android.internal.telephony.CommandsInterface;
  import com.android.internal.telephony.Connection;
  import com.android.internal.telephony.LocaleTracker;
  import com.android.internal.telephony.Phone;
  import com.android.internal.telephony.PhoneConstants;
  import com.android.internal.telephony.ServiceStateTracker;
  import com.android.internal.telephony.SubscriptionController;
  import com.android.internal.telephony.TelephonyProperties;
  import com.android.internal.telephony.dataconnection.DataEnabledSettings;
  import com.android.internal.telephony.dataconnection.DataEnabledSettings.DataEnabledChangedReason;
  import com.android.internal.telephony.gsm.SuppServiceNotification;
  import com.android.internal.telephony.imsphone.ImsPhone.ImsDialArgs;
  import com.android.internal.telephony.metrics.CallQualityMetrics;
  import com.android.internal.telephony.metrics.TelephonyMetrics;
  import com.android.internal.telephony.nano.TelephonyProto.ImsConnectionState;
  import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession;
  import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.ImsCommand;
  import com.android.server.net.NetworkStatsService;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Queue;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.ConcurrentLinkedQueue;
  import java.util.concurrent.Executor;
  import java.util.concurrent.LinkedBlockingQueue;
  import java.util.concurrent.atomic.AtomicInteger;
  import java.util.regex.Pattern;
  
  /**
   * {@hide}
   */
  public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
      static final String LOG_TAG = "ImsPhoneCallTracker";
      static final String VERBOSE_STATE_TAG = "IPCTState";
  
      public interface PhoneStateListener {
          void onPhoneStateChanged(PhoneConstants.State oldState, PhoneConstants.State newState);
      }
  
      public interface SharedPreferenceProxy {
          SharedPreferences getDefaultSharedPreferences(Context context);
      }
  
      public interface PhoneNumberUtilsProxy {
          boolean isEmergencyNumber(String number);
      }
  
      private static final boolean DBG = true;
  
      // When true, dumps the state of ImsPhoneCallTracker after changes to foreground and background
      // calls.  This is helpful for debugging.  It is also possible to enable this at runtime by
      // setting the IPCTState log tag to VERBOSE.
      private static final boolean FORCE_VERBOSE_STATE_LOGGING = false; /* stopship if true */
      private static final boolean VERBOSE_STATE_LOGGING = FORCE_VERBOSE_STATE_LOGGING ||
              Rlog.isLoggable(VERBOSE_STATE_TAG, Log.VERBOSE);
  
      private MmTelFeature.MmTelCapabilities mMmTelCapabilities =
              new MmTelFeature.MmTelCapabilities();
  
      private TelephonyMetrics mMetrics;
      private final Map<String, CallQualityMetrics> mCallQualityMetrics = new ConcurrentHashMap<>();
      private final ConcurrentLinkedQueue<CallQualityMetrics> mCallQualityMetricsHistory =
              new ConcurrentLinkedQueue<>();
      private boolean mCarrierConfigLoaded = false;
  
      private final MmTelFeatureListener mMmTelFeatureListener = new MmTelFeatureListener();
      private class MmTelFeatureListener extends MmTelFeature.Listener {
          @Override
          public void onIncomingCall(IImsCallSession c, Bundle extras) {
              if (DBG) log("onReceive : incoming call intent");
  
              if (mImsManager == null) return;
  
              try {
                  // Network initiated USSD will be treated by mImsUssdListener
                  boolean isUssd = extras.getBoolean(ImsManager.EXTRA_USSD, false);
                  if (isUssd) {
                      if (DBG) log("onReceive : USSD");
                      mUssdSession = mImsManager.takeCall(c, extras, mImsUssdListener);
                      if (mUssdSession != null) {
                          mUssdSession.accept(ImsCallProfile.CALL_TYPE_VOICE);
                      }
                      return;
                  }
  
                  boolean isUnknown = extras.getBoolean(ImsManager.EXTRA_IS_UNKNOWN_CALL, false);
                  if (DBG) {
                      log("onReceive : isUnknown = " + isUnknown
                              + " fg = " + mForegroundCall.getState()
                              + " bg = " + mBackgroundCall.getState());
                  }
  
                  // Normal MT/Unknown call
                  ImsCall imsCall = mImsManager.takeCall(c, extras, mImsCallListener);
                  ImsPhoneConnection conn = new ImsPhoneConnection(mPhone, imsCall,
                          ImsPhoneCallTracker.this,
                          (isUnknown ? mForegroundCall : mRingingCall), isUnknown);
  
                  // If there is an active call.
                  if (mForegroundCall.hasConnections()) {
                      ImsCall activeCall = mForegroundCall.getFirstConnection().getImsCall();
                      if (activeCall != null && imsCall != null) {
                          // activeCall could be null if the foreground call is in a disconnected
                          // state.  If either of the calls is null there is no need to check if
                          // one will be disconnected on answer.
                          boolean answeringWillDisconnect =
                                  shouldDisconnectActiveCallOnAnswer(activeCall, imsCall);
                          conn.setActiveCallDisconnectedOnAnswer(answeringWillDisconnect);
                      }
                  }
                  conn.setAllowAddCallDuringVideoCall(mAllowAddCallDuringVideoCall);
                  addConnection(conn);
  
                  setVideoCallProvider(conn, imsCall);
  
                  TelephonyMetrics.getInstance().writeOnImsCallReceive(mPhone.getPhoneId(),
                          imsCall.getSession());
  
                  if (isUnknown) {
                      mPhone.notifyUnknownConnection(conn);
                  } else {
                      if ((mForegroundCall.getState() != ImsPhoneCall.State.IDLE)
                              || (mBackgroundCall.getState() != ImsPhoneCall.State.IDLE)) {
                          conn.update(imsCall, ImsPhoneCall.State.WAITING);
                      }
  
                      mPhone.notifyNewRingingConnection(conn);
                      mPhone.notifyIncomingRing();
                  }
  
                  updatePhoneState();
                  mPhone.notifyPreciseCallStateChanged();
              } catch (ImsException e) {
                  loge("onReceive : exception " + e);
              } catch (RemoteException e) {
              }
          }
  
          @Override
          public void onVoiceMessageCountUpdate(int count) {
              if (mPhone != null && mPhone.mDefaultPhone != null) {
                  if (DBG) log("onVoiceMessageCountChanged :: count=" + count);
                  mPhone.mDefaultPhone.setVoiceMessageCount(count);
              } else {
                  loge("onVoiceMessageCountUpdate: null phone");
              }
          }
      }
  
      private BroadcastReceiver mReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              if (intent.getAction().equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
                  int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
                          SubscriptionManager.INVALID_SUBSCRIPTION_ID);
                  if (subId == mPhone.getSubId()) {
                      cacheCarrierConfiguration(subId);
                      log("onReceive : Updating mAllowEmergencyVideoCalls = " +
                              mAllowEmergencyVideoCalls);
                  }
              } else if (TelecomManager.ACTION_CHANGE_DEFAULT_DIALER.equals(intent.getAction())) {
                  mDefaultDialerUid.set(getPackageUid(context, intent.getStringExtra(
                          TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME)));
              }
          }
      };
  
      /**
       * Tracks whether we are currently monitoring network connectivity for the purpose of warning
       * the user of an inability to handover from LTE to WIFI for video calls.
       */
      private boolean mIsMonitoringConnectivity = false;
  
      /**
       * Network callback used to schedule the handover check when a wireless network connects.
       */
      private ConnectivityManager.NetworkCallback mNetworkCallback =
              new ConnectivityManager.NetworkCallback() {
                  @Override
                  public void onAvailable(Network network) {
                      Rlog.i(LOG_TAG, "Network available: " + network);
                      scheduleHandoverCheck();
                  }
              };
  
      //***** Constants
  
      static final int MAX_CONNECTIONS = 7;
      static final int MAX_CONNECTIONS_PER_CALL = 5;
  
      // Max number of calls we will keep call quality history for (the history is saved in-memory and
      // included in bug reports).
      private static final int MAX_CALL_QUALITY_HISTORY = 10;
  
      private static final int EVENT_HANGUP_PENDINGMO = 18;
      private static final int EVENT_DIAL_PENDINGMO = 20;
      private static final int EVENT_EXIT_ECBM_BEFORE_PENDINGMO = 21;
      private static final int EVENT_VT_DATA_USAGE_UPDATE = 22;
      private static final int EVENT_DATA_ENABLED_CHANGED = 23;
      private static final int EVENT_CHECK_FOR_WIFI_HANDOVER = 25;
      private static final int EVENT_ON_FEATURE_CAPABILITY_CHANGED = 26;
      private static final int EVENT_SUPP_SERVICE_INDICATION = 27;
      private static final int EVENT_REDIAL_WIFI_E911_CALL = 28;
      private static final int EVENT_REDIAL_WIFI_E911_TIMEOUT = 29;
      private static final int EVENT_ANSWER_WAITING_CALL = 30;
      private static final int EVENT_RESUME_NOW_FOREGROUND_CALL = 31;
      private static final int EVENT_REDIAL_WITHOUT_RTT = 32;
  
      private static final int TIMEOUT_HANGUP_PENDINGMO = 500;
  
      private static final int HANDOVER_TO_WIFI_TIMEOUT_MS = 60000; // ms
  
      private static final int TIMEOUT_REDIAL_WIFI_E911_MS = 10000;
  
      private static final int TIMEOUT_PARTICIPANT_CONNECT_TIME_CACHE_MS = 60000; //ms
  
      // Following values are for mHoldSwitchingState
      private enum HoldSwapState {
          // Not in the middle of a hold/swap operation
          INACTIVE,
          // Pending a single call getting held
          PENDING_SINGLE_CALL_HOLD,
          // Pending a single call getting unheld
          PENDING_SINGLE_CALL_UNHOLD,
          // Pending swapping a active and a held call
          SWAPPING_ACTIVE_AND_HELD,
          // Pending holding a call to answer a call-waiting call
          HOLDING_TO_ANSWER_INCOMING,
          // Pending resuming the foreground call after some kind of failure
          PENDING_RESUME_FOREGROUND_AFTER_FAILURE,
          // Pending holding a call to dial another outgoing call
          HOLDING_TO_DIAL_OUTGOING,
      }
  
      //***** Instance Variables
      @UnsupportedAppUsage
      private ArrayList<ImsPhoneConnection> mConnections = new ArrayList<ImsPhoneConnection>();
      private RegistrantList mVoiceCallEndedRegistrants = new RegistrantList();
      private RegistrantList mVoiceCallStartedRegistrants = new RegistrantList();
  
      @UnsupportedAppUsage
      public ImsPhoneCall mRingingCall = new ImsPhoneCall(this, ImsPhoneCall.CONTEXT_RINGING);
      @UnsupportedAppUsage
      public ImsPhoneCall mForegroundCall = new ImsPhoneCall(this,
              ImsPhoneCall.CONTEXT_FOREGROUND);
      @UnsupportedAppUsage
      public ImsPhoneCall mBackgroundCall = new ImsPhoneCall(this,
              ImsPhoneCall.CONTEXT_BACKGROUND);
      @UnsupportedAppUsage
      public ImsPhoneCall mHandoverCall = new ImsPhoneCall(this, ImsPhoneCall.CONTEXT_HANDOVER);
  
      // Hold aggregated video call data usage for each video call since boot.
      // The ImsCall's call id is the key of the map.
      private final HashMap<Integer, Long> mVtDataUsageMap = new HashMap<>();
      private final Map<String, CacheEntry> mPhoneNumAndConnTime = new ConcurrentHashMap<>();
      private final Queue<CacheEntry> mUnknownPeerConnTime = new LinkedBlockingQueue<>();
  
      private static class CacheEntry {
          private long mCachedTime;
          private long mConnectTime;
          private long mConnectElapsedTime;
          /**
           * The direction of the call;
           * {@link android.telecom.Call.Details#DIRECTION_INCOMING} for incoming calls, or
           * {@link android.telecom.Call.Details#DIRECTION_OUTGOING} for outgoing calls.
           */
          private int mCallDirection;
  
          CacheEntry(long cachedTime, long connectTime, long connectElapsedTime, int callDirection) {
              mCachedTime = cachedTime;
              mConnectTime = connectTime;
              mConnectElapsedTime = connectElapsedTime;
              mCallDirection = callDirection;
          }
      }
  
      private volatile NetworkStats mVtDataUsageSnapshot = null;
      private volatile NetworkStats mVtDataUsageUidSnapshot = null;
  
      private final AtomicInteger mDefaultDialerUid = new AtomicInteger(NetworkStats.UID_ALL);
  
      @UnsupportedAppUsage
      private ImsPhoneConnection mPendingMO;
      private int mClirMode = CommandsInterface.CLIR_DEFAULT;
      @UnsupportedAppUsage
      private Object mSyncHold = new Object();
  
      private ImsCall mUssdSession = null;
      @UnsupportedAppUsage
      private Message mPendingUssd = null;
  
      @UnsupportedAppUsage
      ImsPhone mPhone;
  
      private boolean mDesiredMute = false;    // false = mute off
      @UnsupportedAppUsage
      private boolean mOnHoldToneStarted = false;
      @UnsupportedAppUsage
      private int mOnHoldToneId = -1;
  
      private PhoneConstants.State mState = PhoneConstants.State.IDLE;
  
      private ImsManager mImsManager;
      private ImsUtInterface mUtInterface;
  
      private Call.SrvccState mSrvccState = Call.SrvccState.NONE;
  
      private boolean mIsInEmergencyCall = false;
      private boolean mIsDataEnabled = false;
  
      private int pendingCallClirMode;
      private int mPendingCallVideoState;
      private Bundle mPendingIntentExtras;
      private boolean pendingCallInEcm = false;
      @UnsupportedAppUsage
      private boolean mSwitchingFgAndBgCalls = false;
      private ImsCall mCallExpectedToResume = null;
      @UnsupportedAppUsage
      private boolean mAllowEmergencyVideoCalls = false;
      private boolean mIgnoreDataEnabledChangedForVideoCalls = false;
      private boolean mIsViLteDataMetered = false;
      private boolean mAlwaysPlayRemoteHoldTone = false;
      private boolean mAutoRetryFailedWifiEmergencyCall = false;
      // Tracks the state of our background/foreground calls while a call hold/swap operation is
      // in progress. Values listed above.
      private HoldSwapState mHoldSwitchingState = HoldSwapState.INACTIVE;
  
      private String mLastDialString = null;
      private ImsDialArgs mLastDialArgs = null;
  
      /**
       * Listeners to changes in the phone state.  Intended for use by other interested IMS components
       * without the need to register a full blown {@link android.telephony.PhoneStateListener}.
       */
      private List<PhoneStateListener> mPhoneStateListeners = new ArrayList<>();
  
      /**
       * Carrier configuration option which determines if video calls which have been downgraded to an
       * audio call should be treated as if they are still video calls.
       */
      private boolean mTreatDowngradedVideoCallsAsVideoCalls = false;
  
      /**
       * Carrier configuration option which determines if an ongoing video call over wifi should be
       * dropped when an audio call is answered.
       */
      private boolean mDropVideoCallWhenAnsweringAudioCall = false;
  
      /**
       * Carrier configuration option which determines whether adding a call during a video call
       * should be allowed.
       */
      private boolean mAllowAddCallDuringVideoCall = true;
  
      /**
       * Carrier configuration option which determines whether to notify the connection if a handover
       * to wifi fails.
       */
      private boolean mNotifyVtHandoverToWifiFail = false;
  
      /**
       * Carrier configuration option which determines whether the carrier supports downgrading a
       * TX/RX/TX-RX video call directly to an audio-only call.
       */
      private boolean mSupportDowngradeVtToAudio = false;
  
      /**
       * Carrier configuration option which determines whether the carrier wants to inform the user
       * when a video call is handed over from WIFI to LTE.
       * See {@link CarrierConfigManager#KEY_NOTIFY_HANDOVER_VIDEO_FROM_WIFI_TO_LTE_BOOL} for more
       * information.
       */
      private boolean mNotifyHandoverVideoFromWifiToLTE = false;
  
      /**
       * Carrier configuration option which determines whether the carrier wants to inform the user
       * when a video call is handed over from LTE to WIFI.
       * See {@link CarrierConfigManager#KEY_NOTIFY_HANDOVER_VIDEO_FROM_LTE_TO_WIFI_BOOL} for more
       * information.
       */
      private boolean mNotifyHandoverVideoFromLTEToWifi = false;
  
      /**
       * When {@code} false, indicates that no handover from LTE to WIFI has been attempted during the
       * start of the call.
       * When {@code true}, indicates that the start of call handover from LTE to WIFI has been
       * attempted (it may have succeeded or failed).
       */
      private boolean mHasAttemptedStartOfCallHandover = false;
  
      /**
       * Carrier configuration option which determines whether the carrier supports the
       * {@link VideoProfile#STATE_PAUSED} signalling.
       * See {@link CarrierConfigManager#KEY_SUPPORT_PAUSE_IMS_VIDEO_CALLS_BOOL} for more information.
       */
      private boolean mSupportPauseVideo = false;
  
      /**
       * Carrier configuration option which defines a mapping from pairs of
       * {@link ImsReasonInfo#getCode()} and {@link ImsReasonInfo#getExtraMessage()} values to a new
       * {@code ImsReasonInfo#CODE_*} value.
       *
       * See {@link CarrierConfigManager#KEY_IMS_REASONINFO_MAPPING_STRING_ARRAY}.
       */
      private Map<Pair<Integer, String>, Integer> mImsReasonCodeMap = new ArrayMap<>();
  
  
      /**
       * TODO: Remove this code; it is a workaround.
       * When {@code true}, forces {@link ImsManager#updateImsServiceConfig(boolean)} to
       * be called when an ongoing video call is disconnected.  In some cases, where video pause is
       * supported by the carrier, when {@link #onDataEnabledChanged(boolean, int)} reports that data
       * has been disabled we will pause the video rather than disconnecting the call.  When this
       * happens we need to prevent the IMS service config from being updated, as this will cause VT
       * to be disabled mid-call, resulting in an inability to un-pause the video.
       */
      private boolean mShouldUpdateImsConfigOnDisconnect = false;
  
      /**
       * Default implementation for retrieving shared preferences; uses the actual PreferencesManager.
       */
      private SharedPreferenceProxy mSharedPreferenceProxy = (Context context) -> {
          return PreferenceManager.getDefaultSharedPreferences(context);
      };
  
      /**
       * Default implementation for determining if a number is an emergency number.  Uses the real
       * PhoneNumberUtils.
       */
      private PhoneNumberUtilsProxy mPhoneNumberUtilsProxy = (String string) -> {
          return PhoneNumberUtils.isEmergencyNumber(string);
      };
  
      private final ImsManager.Connector mImsManagerConnector;
  
      //***** Events
  
  
      //***** Constructors
      public ImsPhoneCallTracker(ImsPhone phone) {
          this(phone, phone.getContext().getMainExecutor());
      }
  
      @VisibleForTesting
      public ImsPhoneCallTracker(ImsPhone phone, Executor executor) {
          this.mPhone = phone;
  
          mMetrics = TelephonyMetrics.getInstance();
  
          IntentFilter intentfilter = new IntentFilter();
          intentfilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
          intentfilter.addAction(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER);
          mPhone.getContext().registerReceiver(mReceiver, intentfilter);
          cacheCarrierConfiguration(mPhone.getSubId());
  
          mPhone.getDefaultPhone().getDataEnabledSettings().registerForDataEnabledChanged(
                  this, EVENT_DATA_ENABLED_CHANGED, null);
  
          final TelecomManager telecomManager =
                  (TelecomManager) mPhone.getContext().getSystemService(Context.TELECOM_SERVICE);
          mDefaultDialerUid.set(
                  getPackageUid(mPhone.getContext(), telecomManager.getDefaultDialerPackage()));
  
          long currentTime = SystemClock.elapsedRealtime();
          mVtDataUsageSnapshot = new NetworkStats(currentTime, 1);
          mVtDataUsageUidSnapshot = new NetworkStats(currentTime, 1);
  
          // Allow the executor to be specified for testing.
          mImsManagerConnector = new ImsManager.Connector(phone.getContext(), phone.getPhoneId(),
                  new ImsManager.Connector.Listener() {
                      @Override
                      public void connectionReady(ImsManager manager) throws ImsException {
                          mImsManager = manager;
                          startListeningForCalls();
                      }
  
                      @Override
                      public void connectionUnavailable() {
                          stopListeningForCalls();
                      }
                  }, executor);
          mImsManagerConnector.connect();
      }
  
      /**
       * Test-only method used to mock out access to the shared preferences through the
       * {@link PreferenceManager}.
       * @param sharedPreferenceProxy
       */
      @VisibleForTesting
      public void setSharedPreferenceProxy(SharedPreferenceProxy sharedPreferenceProxy) {
          mSharedPreferenceProxy = sharedPreferenceProxy;
      }
  
      /**
       * Test-only method used to mock out access to the phone number utils class.
       * @param phoneNumberUtilsProxy
       */
      @VisibleForTesting
      public void setPhoneNumberUtilsProxy(PhoneNumberUtilsProxy phoneNumberUtilsProxy) {
          mPhoneNumberUtilsProxy = phoneNumberUtilsProxy;
      }
  
      /**
       * Test-only method used to set the ImsService retry timeout.
       */
      @VisibleForTesting
      public void setRetryTimeout(ImsManager.Connector.RetryTimeout retryTimeout) {
          mImsManagerConnector.mRetryTimeout = retryTimeout;
      }
  
      private int getPackageUid(Context context, String pkg) {
          if (pkg == null) {
              return NetworkStats.UID_ALL;
          }
  
          // Initialize to UID_ALL so at least it can be counted to overall data usage if
          // the dialer's package uid is not available.
          int uid = NetworkStats.UID_ALL;
          try {
              uid = context.getPackageManager().getPackageUid(pkg, 0);
          } catch (PackageManager.NameNotFoundException e) {
              loge("Cannot find package uid. pkg = " + pkg);
          }
          return uid;
      }
  
      private void startListeningForCalls() throws ImsException {
          log("startListeningForCalls");
          mImsManager.open(mMmTelFeatureListener);
          mImsManager.addRegistrationCallback(mImsRegistrationCallback);
          mImsManager.addCapabilitiesCallback(mImsCapabilityCallback);
  
          mImsManager.setConfigListener(mImsConfigListener);
  
          mImsManager.getConfigInterface().addConfigCallback(mConfigCallback);
  
          // Get the ECBM interface and set IMSPhone's listener object for notifications
          getEcbmInterface().setEcbmStateListener(mPhone.getImsEcbmStateListener());
          if (mPhone.isInEcm()) {
              // Call exit ECBM which will invoke onECBMExited
              mPhone.exitEmergencyCallbackMode();
          }
          int mPreferredTtyMode = Settings.Secure.getInt(
                  mPhone.getContext().getContentResolver(),
                  Settings.Secure.PREFERRED_TTY_MODE,
                  Phone.TTY_MODE_OFF);
          mImsManager.setUiTTYMode(mPhone.getContext(), mPreferredTtyMode, null);
  
          ImsMultiEndpoint multiEndpoint = getMultiEndpointInterface();
          if (multiEndpoint != null) {
              multiEndpoint.setExternalCallStateListener(
                      mPhone.getExternalCallTracker().getExternalCallStateListener());
          }
  
          //Set UT interface listener to receive UT indications.
          mUtInterface = getUtInterface();
          if (mUtInterface != null) {
              mUtInterface.registerForSuppServiceIndication(this,
                      EVENT_SUPP_SERVICE_INDICATION, null);
          }
  
          if (mCarrierConfigLoaded) {
              mImsManager.updateImsServiceConfig(true);
          }
          // For compatibility with apps that still use deprecated intent
          sendImsServiceStateIntent(ImsManager.ACTION_IMS_SERVICE_UP);
      }
  
      private void stopListeningForCalls() {
          log("stopListeningForCalls");
          resetImsCapabilities();
          // Only close on valid session.
          if (mImsManager != null) {
              try {
                  mImsManager.getConfigInterface().removeConfigCallback(mConfigCallback.getBinder());
              } catch (ImsException e) {
                  Log.w(LOG_TAG, "stopListeningForCalls: unable to remove config callback.");
              }
              mImsManager.close();
          }
          // For compatibility with apps that still use deprecated intent
          sendImsServiceStateIntent(ImsManager.ACTION_IMS_SERVICE_DOWN);
      }
  
      private void sendImsServiceStateIntent(String intentAction) {
          Intent intent = new Intent(intentAction);
          intent.putExtra(ImsManager.EXTRA_PHONE_ID, mPhone.getPhoneId());
          if (mPhone != null && mPhone.getContext() != null) {
              mPhone.getContext().sendBroadcast(intent);
          }
      }
  
      public void dispose() {
          if (DBG) log("dispose");
          mRingingCall.dispose();
          mBackgroundCall.dispose();
          mForegroundCall.dispose();
          mHandoverCall.dispose();
  
          clearDisconnected();
          if (mUtInterface != null) {
              mUtInterface.unregisterForSuppServiceIndication(this);
          }
          mPhone.getContext().unregisterReceiver(mReceiver);
          mPhone.getDefaultPhone().getDataEnabledSettings().unregisterForDataEnabledChanged(this);
          mImsManagerConnector.disconnect();
      }
  
      @Override
      protected void finalize() {
          log("ImsPhoneCallTracker finalized");
      }
  
      //***** Instance Methods
  
      //***** Public Methods
      @Override
      public void registerForVoiceCallStarted(Handler h, int what, Object obj) {
          Registrant r = new Registrant(h, what, obj);
          mVoiceCallStartedRegistrants.add(r);
      }
  
      @Override
      public void unregisterForVoiceCallStarted(Handler h) {
          mVoiceCallStartedRegistrants.remove(h);
      }
  
      @Override
      public void registerForVoiceCallEnded(Handler h, int what, Object obj) {
          Registrant r = new Registrant(h, what, obj);
          mVoiceCallEndedRegistrants.add(r);
      }
  
      @Override
      public void unregisterForVoiceCallEnded(Handler h) {
          mVoiceCallEndedRegistrants.remove(h);
      }
  
      public int getClirMode() {
          if (mSharedPreferenceProxy != null && mPhone.getDefaultPhone() != null) {
              SharedPreferences sp = mSharedPreferenceProxy.getDefaultSharedPreferences(
                      mPhone.getContext());
              return sp.getInt(Phone.CLIR_KEY + mPhone.getDefaultPhone().getPhoneId(),
                      CommandsInterface.CLIR_DEFAULT);
          } else {
              loge("dial; could not get default CLIR mode.");
              return CommandsInterface.CLIR_DEFAULT;
          }
      }
  
      @UnsupportedAppUsage
      public Connection dial(String dialString, int videoState, Bundle intentExtras) throws
              CallStateException {
          ImsPhone.ImsDialArgs dialArgs =  new ImsPhone.ImsDialArgs.Builder()
                  .setIntentExtras(intentExtras)
                  .setVideoState(videoState)
                  .setClirMode(getClirMode())
                  .build();
          return dial(dialString, dialArgs);
      }
  
      public synchronized Connection dial(String dialString, ImsPhone.ImsDialArgs dialArgs)
              throws CallStateException {
          boolean isPhoneInEcmMode = isPhoneInEcbMode();
          boolean isEmergencyNumber = mPhoneNumberUtilsProxy.isEmergencyNumber(dialString);
  
          if (!shouldNumberBePlacedOnIms(isEmergencyNumber, dialString)) {
              Rlog.i(LOG_TAG, "dial: shouldNumberBePlacedOnIms = false");
              throw new CallStateException(CS_FALLBACK);
          }
  
          int clirMode = dialArgs.clirMode;
          int videoState = dialArgs.videoState;
  
          if (DBG) log("dial clirMode=" + clirMode);
          if (isEmergencyNumber) {
              clirMode = CommandsInterface.CLIR_SUPPRESSION;
              if (DBG) log("dial emergency call, set clirModIe=" + clirMode);
          }
  
          // note that this triggers call state changed notif
          clearDisconnected();
  
          if (mImsManager == null) {
              throw new CallStateException("service not available");
          }
  
          // See if there are any issues which preclude placing a call; throw a CallStateException
          // if there is.
          checkForDialIssues();
  
          if (isPhoneInEcmMode && isEmergencyNumber) {
              handleEcmTimer(ImsPhone.CANCEL_ECM_TIMER);
          }
  
          // If the call is to an emergency number and the carrier does not support video emergency
          // calls, dial as an audio-only call.
          if (isEmergencyNumber && VideoProfile.isVideo(videoState) &&
                  !mAllowEmergencyVideoCalls) {
              loge("dial: carrier does not support video emergency calls; downgrade to audio-only");
              videoState = VideoProfile.STATE_AUDIO_ONLY;
          }
  
          boolean holdBeforeDial = false;
  
          // The new call must be assigned to the foreground call.
          // That call must be idle, so place anything that's
          // there on hold
          if (mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE) {
              if (mBackgroundCall.getState() != ImsPhoneCall.State.IDLE) {
                  //we should have failed in checkForDialIssues above before we get here
                  throw new CallStateException(CallStateException.ERROR_TOO_MANY_CALLS,
                          "Already too many ongoing calls.");
              }
              // foreground call is empty for the newly dialed connection
              holdBeforeDial = true;
              // Cache the video state for pending MO call.
              mPendingCallVideoState = videoState;
              mPendingIntentExtras = dialArgs.intentExtras;
              holdActiveCallForPendingMo();
          }
  
          ImsPhoneCall.State fgState = ImsPhoneCall.State.IDLE;
          ImsPhoneCall.State bgState = ImsPhoneCall.State.IDLE;
  
          mClirMode = clirMode;
  
          synchronized (mSyncHold) {
              if (holdBeforeDial) {
                  fgState = mForegroundCall.getState();
                  bgState = mBackgroundCall.getState();
  
                  //holding foreground call failed
                  if (fgState == ImsPhoneCall.State.ACTIVE) {
                      throw new CallStateException("cannot dial in current state");
                  }
  
                  //holding foreground call succeeded
                  if (bgState == ImsPhoneCall.State.HOLDING) {
                      holdBeforeDial = false;
                  }
              }
  
              mLastDialString = dialString;
              mLastDialArgs = dialArgs;
              mPendingMO = new ImsPhoneConnection(mPhone,
                      checkForTestEmergencyNumber(dialString), this, mForegroundCall,
                      isEmergencyNumber);
              if (isEmergencyNumber && dialArgs != null && dialArgs.intentExtras != null) {
                  Rlog.i(LOG_TAG, "dial ims emergency dialer: " + dialArgs.intentExtras.getBoolean(
                          TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL));
                  mPendingMO.setHasKnownUserIntentEmergency(dialArgs.intentExtras.getBoolean(
                          TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL));
              }
              mPendingMO.setVideoState(videoState);
              if (dialArgs.rttTextStream != null) {
                  log("dial: setting RTT stream on mPendingMO");
                  mPendingMO.setCurrentRttTextStream(dialArgs.rttTextStream);
              }
          }
          addConnection(mPendingMO);
  
          if (!holdBeforeDial) {
              if ((!isPhoneInEcmMode) || (isPhoneInEcmMode && isEmergencyNumber)) {
                  dialInternal(mPendingMO, clirMode, videoState, dialArgs.intentExtras);
              } else {
                  try {
                      getEcbmInterface().exitEmergencyCallbackMode();
                  } catch (ImsException e) {
                      e.printStackTrace();
                      throw new CallStateException("service not available");
                  }
                  mPhone.setOnEcbModeExitResponse(this, EVENT_EXIT_ECM_RESPONSE_CDMA, null);
                  pendingCallClirMode = clirMode;
                  mPendingCallVideoState = videoState;
                  pendingCallInEcm = true;
              }
          }
  
          updatePhoneState();
          mPhone.notifyPreciseCallStateChanged();
  
          return mPendingMO;
      }
  
      boolean isImsServiceReady() {
          if (mImsManager == null) {
              return false;
          }
  
          return mImsManager.isServiceReady();
      }
  
      private boolean shouldNumberBePlacedOnIms(boolean isEmergency, String number) {
          int processCallResult;
          try {
              if (mImsManager != null) {
                  processCallResult = mImsManager.shouldProcessCall(isEmergency,
                          new String[]{number});
                  Rlog.i(LOG_TAG, "shouldProcessCall: number: " + Rlog.pii(LOG_TAG, number)
                          + ", result: " + processCallResult);
              } else {
                  Rlog.w(LOG_TAG, "ImsManager unavailable, shouldProcessCall returning false.");
                  return false;
              }
          } catch (ImsException e) {
              Rlog.w(LOG_TAG, "ImsService unavailable, shouldProcessCall returning false.");
              return false;
          }
          switch(processCallResult) {
              case MmTelFeature.PROCESS_CALL_IMS: {
                  // The ImsService wishes to place the call over IMS
                  return true;
              }
              case MmTelFeature.PROCESS_CALL_CSFB: {
                  Rlog.i(LOG_TAG, "shouldProcessCall: place over CSFB instead.");
                  return false;
              }
              default: {
                  Rlog.w(LOG_TAG, "shouldProcessCall returned unknown result.");
                  return false;
              }
          }
      }
  
      /**
       * Caches frequently used carrier configuration items locally.
       *
       * @param subId The sub id.
       */
      private void cacheCarrierConfiguration(int subId) {
          CarrierConfigManager carrierConfigManager = (CarrierConfigManager)
                  mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
          if (carrierConfigManager == null
                  || !SubscriptionController.getInstance().isActiveSubId(subId)) {
              loge("cacheCarrierConfiguration: No carrier config service found" + " "
                      + "or not active subId = " + subId);
              mCarrierConfigLoaded = false;
              return;
          }
  
          PersistableBundle carrierConfig = carrierConfigManager.getConfigForSubId(subId);
          if (carrierConfig == null) {
              loge("cacheCarrierConfiguration: Empty carrier config.");
              mCarrierConfigLoaded = false;
              return;
          }
          mCarrierConfigLoaded = true;
  
          updateCarrierConfigCache(carrierConfig);
      }
  
      /**
       * Updates the local carrier config cache from a bundle obtained from the carrier config
       * manager.  Also supports unit testing by injecting configuration at test time.
       * @param carrierConfig The config bundle.
       */
      @VisibleForTesting
      public void updateCarrierConfigCache(PersistableBundle carrierConfig) {
          mAllowEmergencyVideoCalls =
                  carrierConfig.getBoolean(CarrierConfigManager.KEY_ALLOW_EMERGENCY_VIDEO_CALLS_BOOL);
          mTreatDowngradedVideoCallsAsVideoCalls =
                  carrierConfig.getBoolean(
                          CarrierConfigManager.KEY_TREAT_DOWNGRADED_VIDEO_CALLS_AS_VIDEO_CALLS_BOOL);
          mDropVideoCallWhenAnsweringAudioCall =
                  carrierConfig.getBoolean(
                          CarrierConfigManager.KEY_DROP_VIDEO_CALL_WHEN_ANSWERING_AUDIO_CALL_BOOL);
          mAllowAddCallDuringVideoCall =
                  carrierConfig.getBoolean(
                          CarrierConfigManager.KEY_ALLOW_ADD_CALL_DURING_VIDEO_CALL_BOOL);
          mNotifyVtHandoverToWifiFail = carrierConfig.getBoolean(
                  CarrierConfigManager.KEY_NOTIFY_VT_HANDOVER_TO_WIFI_FAILURE_BOOL);
          mSupportDowngradeVtToAudio = carrierConfig.getBoolean(
                  CarrierConfigManager.KEY_SUPPORT_DOWNGRADE_VT_TO_AUDIO_BOOL);
          mNotifyHandoverVideoFromWifiToLTE = carrierConfig.getBoolean(
                  CarrierConfigManager.KEY_NOTIFY_HANDOVER_VIDEO_FROM_WIFI_TO_LTE_BOOL);
          mNotifyHandoverVideoFromLTEToWifi = carrierConfig.getBoolean(
                  CarrierConfigManager.KEY_NOTIFY_HANDOVER_VIDEO_FROM_LTE_TO_WIFI_BOOL);
          mIgnoreDataEnabledChangedForVideoCalls = carrierConfig.getBoolean(
                  CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS);
          mIsViLteDataMetered = carrierConfig.getBoolean(
                  CarrierConfigManager.KEY_VILTE_DATA_IS_METERED_BOOL);
          mSupportPauseVideo = carrierConfig.getBoolean(
                  CarrierConfigManager.KEY_SUPPORT_PAUSE_IMS_VIDEO_CALLS_BOOL);
          mAlwaysPlayRemoteHoldTone = carrierConfig.getBoolean(
                  CarrierConfigManager.KEY_ALWAYS_PLAY_REMOTE_HOLD_TONE_BOOL);
          mAutoRetryFailedWifiEmergencyCall = carrierConfig.getBoolean(
                  CarrierConfigManager.KEY_AUTO_RETRY_FAILED_WIFI_EMERGENCY_CALL);
  
          String[] mappings = carrierConfig
                  .getStringArray(CarrierConfigManager.KEY_IMS_REASONINFO_MAPPING_STRING_ARRAY);
          if (mappings != null && mappings.length > 0) {
              for (String mapping : mappings) {
                  String[] values = mapping.split(Pattern.quote("|"));
                  if (values.length != 3) {
                      continue;
                  }
  
                  try {
                      Integer fromCode;
                      if (values[0].equals("*")) {
                          fromCode = null;
                      } else {
                          fromCode = Integer.parseInt(values[0]);
                      }
                      String message = values[1];
                      if (message == null) {
                          message = "";
                      }
                      int toCode = Integer.parseInt(values[2]);
  
                      addReasonCodeRemapping(fromCode, message, toCode);
                      log("Loaded ImsReasonInfo mapping : fromCode = " +
                              fromCode == null ? "any" : fromCode + " ; message = " +
                              message + " ; toCode = " + toCode);
                  } catch (NumberFormatException nfe) {
                      loge("Invalid ImsReasonInfo mapping found: " + mapping);
                  }
              }
          } else {
              log("No carrier ImsReasonInfo mappings defined.");
          }
      }
  
      @UnsupportedAppUsage
      private void handleEcmTimer(int action) {
          mPhone.handleTimerInEmergencyCallbackMode(action);
          switch (action) {
              case ImsPhone.CANCEL_ECM_TIMER:
                  break;
              case ImsPhone.RESTART_ECM_TIMER:
                  break;
              default:
                  log("handleEcmTimer, unsupported action " + action);
          }
      }
  
      private void dialInternal(ImsPhoneConnection conn, int clirMode, int videoState,
              Bundle intentExtras) {
  
          if (conn == null) {
              return;
          }
  
          if (conn.getAddress()== null || conn.getAddress().length() == 0
                  || conn.getAddress().indexOf(PhoneNumberUtils.WILD) >= 0) {
              // Phone number is invalid
              conn.setDisconnectCause(DisconnectCause.INVALID_NUMBER);
              sendEmptyMessageDelayed(EVENT_HANGUP_PENDINGMO, TIMEOUT_HANGUP_PENDINGMO);
              return;
          }
  
          // Always unmute when initiating a new call
          setMute(false);
          boolean isEmergencyCall = mPhoneNumberUtilsProxy.isEmergencyNumber(conn.getAddress());
          int serviceType = isEmergencyCall
                  ? ImsCallProfile.SERVICE_TYPE_EMERGENCY : ImsCallProfile.SERVICE_TYPE_NORMAL;
          int callType = ImsCallProfile.getCallTypeFromVideoState(videoState);
          //TODO(vt): Is this sufficient?  At what point do we know the video state of the call?
          conn.setVideoState(videoState);
  
          try {
              String[] callees = new String[] { conn.getAddress() };
              ImsCallProfile profile = mImsManager.createCallProfile(serviceType, callType);
              profile.setCallExtraInt(ImsCallProfile.EXTRA_OIR, clirMode);
  
              if (isEmergencyCall) {
                  // Set emergency call information in ImsCallProfile
                  setEmergencyCallInfo(profile, conn);
              }
  
              // Translate call subject intent-extra from Telecom-specific extra key to the
              // ImsCallProfile key.
              if (intentExtras != null) {
                  if (intentExtras.containsKey(android.telecom.TelecomManager.EXTRA_CALL_SUBJECT)) {
                      intentExtras.putString(ImsCallProfile.EXTRA_DISPLAY_TEXT,
                              cleanseInstantLetteringMessage(intentExtras.getString(
                                      android.telecom.TelecomManager.EXTRA_CALL_SUBJECT))
                      );
                  }
  
                  if (conn.hasRttTextStream()) {
                      profile.mMediaProfile.mRttMode = ImsStreamMediaProfile.RTT_MODE_FULL;
                  }
  
                  if (intentExtras.containsKey(ImsCallProfile.EXTRA_IS_CALL_PULL)) {
                      profile.mCallExtras.putBoolean(ImsCallProfile.EXTRA_IS_CALL_PULL,
                              intentExtras.getBoolean(ImsCallProfile.EXTRA_IS_CALL_PULL));
                      int dialogId = intentExtras.getInt(
                              ImsExternalCallTracker.EXTRA_IMS_EXTERNAL_CALL_ID);
                      conn.setIsPulledCall(true);
                      conn.setPulledDialogId(dialogId);
                  }
  
                  // Pack the OEM-specific call extras.
                  profile.mCallExtras.putBundle(ImsCallProfile.EXTRA_OEM_EXTRAS, intentExtras);
  
                  // NOTE: Extras to be sent over the network are packed into the
                  // intentExtras individually, with uniquely defined keys.
                  // These key-value pairs are processed by IMS Service before
                  // being sent to the lower layers/to the network.
              }
  
              ImsCall imsCall = mImsManager.makeCall(profile, callees, mImsCallListener);
              conn.setImsCall(imsCall);
  
              mMetrics.writeOnImsCallStart(mPhone.getPhoneId(),
                      imsCall.getSession());
  
              setVideoCallProvider(conn, imsCall);
              conn.setAllowAddCallDuringVideoCall(mAllowAddCallDuringVideoCall);
          } catch (ImsException e) {
              loge("dialInternal : " + e);
              conn.setDisconnectCause(DisconnectCause.ERROR_UNSPECIFIED);
              sendEmptyMessageDelayed(EVENT_HANGUP_PENDINGMO, TIMEOUT_HANGUP_PENDINGMO);
              retryGetImsService();
          } catch (RemoteException e) {
          }
      }
  
      /**
       * Accepts a call with the specified video state.  The video state is the video state that the
       * user has agreed upon in the InCall UI.
       *
       * @param videoState The video State
       * @throws CallStateException
       */
      public void acceptCall(int videoState) throws CallStateException {
          if (DBG) log("acceptCall");
  
          if (mForegroundCall.getState().isAlive()
                  && mBackgroundCall.getState().isAlive()) {
              throw new CallStateException("cannot accept call");
          }
  
          if ((mRingingCall.getState() == ImsPhoneCall.State.WAITING)
                  && mForegroundCall.getState().isAlive()) {
              setMute(false);
  
              boolean answeringWillDisconnect = false;
              ImsCall activeCall = mForegroundCall.getImsCall();
              ImsCall ringingCall = mRingingCall.getImsCall();
              if (mForegroundCall.hasConnections() && mRingingCall.hasConnections()) {
                  answeringWillDisconnect =
                          shouldDisconnectActiveCallOnAnswer(activeCall, ringingCall);
              }
  
              // Cache video state for pending MT call.
              mPendingCallVideoState = videoState;
  
              if (answeringWillDisconnect) {
                  // We need to disconnect the foreground call before answering the background call.
                  mForegroundCall.hangup();
                  try {
                      ringingCall.accept(ImsCallProfile.getCallTypeFromVideoState(videoState));
                  } catch (ImsException e) {
                      throw new CallStateException("cannot accept call");
                  }
              } else {
                  holdActiveCallForWaitingCall();
              }
          } else if (mRingingCall.getState().isRinging()) {
              if (DBG) log("acceptCall: incoming...");
              // Always unmute when answering a new call
              setMute(false);
              try {
                  ImsCall imsCall = mRingingCall.getImsCall();
                  if (imsCall != null) {
                      imsCall.accept(ImsCallProfile.getCallTypeFromVideoState(videoState));
                      mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
                              ImsCommand.IMS_CMD_ACCEPT);
                  } else {
                      throw new CallStateException("no valid ims call");
                  }
              } catch (ImsException e) {
                  throw new CallStateException("cannot accept call");
              }
          } else {
              throw new CallStateException("phone not ringing");
          }
      }
  
      public void rejectCall () throws CallStateException {
          if (DBG) log("rejectCall");
  
          if (mRingingCall.getState().isRinging()) {
              hangup(mRingingCall);
          } else {
              throw new CallStateException("phone not ringing");
          }
      }
  
      /**
       * Set the emergency call information if it is an emergency call.
       */
      private void setEmergencyCallInfo(ImsCallProfile profile, Connection conn) {
          EmergencyNumber num = conn.getEmergencyNumberInfo();
          if (num != null) {
              profile.setEmergencyCallInfo(num, conn.hasKnownUserIntentEmergency());
          }
      }
  
      @UnsupportedAppUsage
      private void switchAfterConferenceSuccess() {
          if (DBG) log("switchAfterConferenceSuccess fg =" + mForegroundCall.getState() +
                  ", bg = " + mBackgroundCall.getState());
  
          if (mBackgroundCall.getState() == ImsPhoneCall.State.HOLDING) {
              log("switchAfterConferenceSuccess");
              mForegroundCall.switchWith(mBackgroundCall);
          }
      }
  
      private void holdActiveCallForPendingMo() throws CallStateException {
          if (mHoldSwitchingState == HoldSwapState.PENDING_SINGLE_CALL_HOLD
                  || mHoldSwitchingState == HoldSwapState.SWAPPING_ACTIVE_AND_HELD) {
              logi("Ignoring hold request while already holding or swapping");
              return;
          }
          ImsCall callToHold = mForegroundCall.getImsCall();
  
          mHoldSwitchingState = HoldSwapState.HOLDING_TO_DIAL_OUTGOING;
          logHoldSwapState("holdActiveCallForPendingMo");
  
          mForegroundCall.switchWith(mBackgroundCall);
          try {
              callToHold.hold();
              mMetrics.writeOnImsCommand(mPhone.getPhoneId(), callToHold.getSession(),
                      ImsCommand.IMS_CMD_HOLD);
          } catch (ImsException e) {
              mForegroundCall.switchWith(mBackgroundCall);
              throw new CallStateException(e.getMessage());
          }
      }
  
      /**
       * Holds the active call, possibly resuming the already-held background call if it exists.
       */
      public void holdActiveCall() throws CallStateException {
          if (mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE) {
              if (mHoldSwitchingState == HoldSwapState.PENDING_SINGLE_CALL_HOLD
                      || mHoldSwitchingState == HoldSwapState.SWAPPING_ACTIVE_AND_HELD) {
                  logi("Ignoring hold request while already holding or swapping");
                  return;
              }
              ImsCall callToHold = mForegroundCall.getImsCall();
              if (mBackgroundCall.getState().isAlive()) {
                  mCallExpectedToResume = mBackgroundCall.getImsCall();
                  mHoldSwitchingState = HoldSwapState.SWAPPING_ACTIVE_AND_HELD;
              } else {
                  mHoldSwitchingState = HoldSwapState.PENDING_SINGLE_CALL_HOLD;
              }
              logHoldSwapState("holdActiveCall");
              mForegroundCall.switchWith(mBackgroundCall);
              try {
                  callToHold.hold();
                  mMetrics.writeOnImsCommand(mPhone.getPhoneId(), callToHold.getSession(),
                          ImsCommand.IMS_CMD_HOLD);
              } catch (ImsException e) {
                  mForegroundCall.switchWith(mBackgroundCall);
                  throw new CallStateException(e.getMessage());
              }
          }
      }
  
      /**
       * Hold the currently active call in order to answer the waiting call.
       */
      public void holdActiveCallForWaitingCall() throws CallStateException {
          boolean switchingWithWaitingCall = !mBackgroundCall.getState().isAlive()
                  && mRingingCall.getState() == ImsPhoneCall.State.WAITING;
          if (switchingWithWaitingCall) {
              ImsCall callToHold = mForegroundCall.getImsCall();
              mHoldSwitchingState = HoldSwapState.HOLDING_TO_ANSWER_INCOMING;
              mForegroundCall.switchWith(mBackgroundCall);
              logHoldSwapState("holdActiveCallForWaitingCall");
              try {
                  callToHold.hold();
                  mMetrics.writeOnImsCommand(mPhone.getPhoneId(), callToHold.getSession(),
                          ImsCommand.IMS_CMD_HOLD);
              } catch (ImsException e) {
                  mForegroundCall.switchWith(mBackgroundCall);
                  throw new CallStateException(e.getMessage());
              }
          }
      }
  
      /**
       * Unhold the currently held call.
       */
      void unholdHeldCall() throws CallStateException {
          try {
              ImsCall imsCall = mBackgroundCall.getImsCall();
              if (mHoldSwitchingState == HoldSwapState.PENDING_SINGLE_CALL_UNHOLD
                      || mHoldSwitchingState == HoldSwapState.SWAPPING_ACTIVE_AND_HELD) {
                  logi("Ignoring unhold request while already unholding or swapping");
                  return;
              }
              if (imsCall != null) {
                  mCallExpectedToResume = imsCall;
                  mHoldSwitchingState = HoldSwapState.PENDING_SINGLE_CALL_UNHOLD;
                  mForegroundCall.switchWith(mBackgroundCall);
                  logHoldSwapState("unholdCurrentCall");
                  imsCall.resume();
                  mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
                          ImsCommand.IMS_CMD_RESUME);
              }
          } catch (ImsException e) {
              throw new CallStateException(e.getMessage());
          }
      }
  
      private void resumeForegroundCall() throws ImsException {
          //resume foreground call after holding background call
          //they were switched before holding
          ImsCall imsCall = mForegroundCall.getImsCall();
          if (imsCall != null) {
              imsCall.resume();
              mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
                      ImsCommand.IMS_CMD_RESUME);
          }
      }
  
      private void answerWaitingCall() throws ImsException {
          //accept waiting call after holding background call
          ImsCall imsCall = mRingingCall.getImsCall();
          if (imsCall != null) {
              imsCall.accept(
                      ImsCallProfile.getCallTypeFromVideoState(mPendingCallVideoState));
              mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
                      ImsCommand.IMS_CMD_ACCEPT);
          }
      }
  
      // Clean up expired cache entries.
      private void maintainConnectTimeCache() {
          long threshold = SystemClock.elapsedRealtime() - TIMEOUT_PARTICIPANT_CONNECT_TIME_CACHE_MS;
          // The cached time is the system elapsed millisecond when the CacheEntry is created.
          mPhoneNumAndConnTime.entrySet().removeIf(e -> e.getValue().mCachedTime < threshold);
          // Remove all the cached records which are older than current caching threshold. Since the
          // queue is FIFO, keep polling records until the queue is empty or the head of the queue is
          // fresh enough.
          while (!mUnknownPeerConnTime.isEmpty()
                  && mUnknownPeerConnTime.peek().mCachedTime < threshold) {
              mUnknownPeerConnTime.poll();
          }
      }
  
      private void cacheConnectionTimeWithPhoneNumber(@NonNull ImsPhoneConnection connection) {
          int callDirection =
                  connection.isIncoming() ? android.telecom.Call.Details.DIRECTION_INCOMING
                          : android.telecom.Call.Details.DIRECTION_OUTGOING;
          CacheEntry cachedConnectTime = new CacheEntry(SystemClock.elapsedRealtime(),
                  connection.getConnectTime(), connection.getConnectTimeReal(), callDirection);
          maintainConnectTimeCache();
          if (PhoneConstants.PRESENTATION_ALLOWED == connection.getNumberPresentation()) {
              // In case of merging calls with the same number, use the latest connect time. Since
              // that call might be dropped and re-connected. So if the connectTime is earlier than
              // the cache, skip.
              String phoneNumber = getFormattedPhoneNumber(connection.getAddress());
              if (mPhoneNumAndConnTime.containsKey(phoneNumber)
                      && connection.getConnectTime()
                          <= mPhoneNumAndConnTime.get(phoneNumber).mConnectTime) {
                  // Use the latest connect time.
                  return;
              }
              mPhoneNumAndConnTime.put(phoneNumber, cachedConnectTime);
          } else {
              mUnknownPeerConnTime.add(cachedConnectTime);
          }
      }
  
      private CacheEntry findConnectionTimeUsePhoneNumber(
              @NonNull ConferenceParticipant participant) {
          maintainConnectTimeCache();
          if (PhoneConstants.PRESENTATION_ALLOWED == participant.getParticipantPresentation()) {
              if (participant.getHandle() == null
                      || participant.getHandle().getSchemeSpecificPart() == null) {
                  return null;
              }
  
              String number = ConferenceParticipant.getParticipantAddress(participant.getHandle(),
                      getCountryIso()).getSchemeSpecificPart();
              if (TextUtils.isEmpty(number)) {
                  return null;
              }
              String formattedNumber = getFormattedPhoneNumber(number);
              return mPhoneNumAndConnTime.get(formattedNumber);
          } else {
              return mUnknownPeerConnTime.poll();
          }
      }
  
      private String getFormattedPhoneNumber(String number) {
          String countryIso = getCountryIso();
          if (countryIso == null) {
              return number;
          }
          String phoneNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
          return phoneNumber == null ? number : phoneNumber;
      }
  
      private String getCountryIso() {
          int subId = mPhone.getSubId();
          SubscriptionInfo info =
                  SubscriptionManager.from(mPhone.getContext()).getActiveSubscriptionInfo(subId);
          return info == null ? null : info.getCountryIso();
      }
  
      public void
      conference() {
          ImsCall fgImsCall = mForegroundCall.getImsCall();
          if (fgImsCall == null) {
              log("conference no foreground ims call");
              return;
          }
  
          ImsCall bgImsCall = mBackgroundCall.getImsCall();
          if (bgImsCall == null) {
              log("conference no background ims call");
              return;
          }
  
          if (fgImsCall.isCallSessionMergePending()) {
              log("conference: skip; foreground call already in process of merging.");
              return;
          }
  
          if (bgImsCall.isCallSessionMergePending()) {
              log("conference: skip; background call already in process of merging.");
              return;
          }
  
          // Keep track of the connect time of the earliest call so that it can be set on the
          // {@code ImsConference} when it is created.
          long foregroundConnectTime = mForegroundCall.getEarliestConnectTime();
          long backgroundConnectTime = mBackgroundCall.getEarliestConnectTime();
          long conferenceConnectTime;
          if (foregroundConnectTime > 0 && backgroundConnectTime > 0) {
              conferenceConnectTime = Math.min(mForegroundCall.getEarliestConnectTime(),
                      mBackgroundCall.getEarliestConnectTime());
              log("conference - using connect time = " + conferenceConnectTime);
          } else if (foregroundConnectTime > 0) {
              log("conference - bg call connect time is 0; using fg = " + foregroundConnectTime);
              conferenceConnectTime = foregroundConnectTime;
          } else {
              log("conference - fg call connect time is 0; using bg = " + backgroundConnectTime);
              conferenceConnectTime = backgroundConnectTime;
          }
  
          String foregroundId = "";
          ImsPhoneConnection foregroundConnection = mForegroundCall.getFirstConnection();
          if (foregroundConnection != null) {
              foregroundConnection.setConferenceConnectTime(conferenceConnectTime);
              foregroundConnection.handleMergeStart();
              foregroundId = foregroundConnection.getTelecomCallId();
              cacheConnectionTimeWithPhoneNumber(foregroundConnection);
          }
          String backgroundId = "";
          ImsPhoneConnection backgroundConnection = findConnection(bgImsCall);
          if (backgroundConnection != null) {
              backgroundConnection.handleMergeStart();
              backgroundId = backgroundConnection.getTelecomCallId();
              cacheConnectionTimeWithPhoneNumber(backgroundConnection);
          }
          log("conference: fgCallId=" + foregroundId + ", bgCallId=" + backgroundId);
  
          try {
              fgImsCall.merge(bgImsCall);
          } catch (ImsException e) {
              log("conference " + e.getMessage());
          }
      }
  
      public void
      explicitCallTransfer() {
          //TODO : implement
      }
  
      @UnsupportedAppUsage
      public void
      clearDisconnected() {
          if (DBG) log("clearDisconnected");
  
          internalClearDisconnected();
  
          updatePhoneState();
          mPhone.notifyPreciseCallStateChanged();
      }
  
      public boolean
      canConference() {
          return mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE
              && mBackgroundCall.getState() == ImsPhoneCall.State.HOLDING
              && !mBackgroundCall.isFull()
              && !mForegroundCall.isFull();
      }
  
      /**
       * Determines if there are issues which would preclude dialing an outgoing call.  Throws a
       * {@link CallStateException} if there is an issue.
       * @throws CallStateException
       */
      public void checkForDialIssues() throws CallStateException {
          String disableCall = SystemProperties.get(
                  TelephonyProperties.PROPERTY_DISABLE_CALL, "false");
          if (disableCall.equals("true")) {
              throw new CallStateException(CallStateException.ERROR_CALLING_DISABLED,
                      "ro.telephony.disable-call has been used to disable calling.");
          }
          if (mPendingMO != null) {
              throw new CallStateException(CallStateException.ERROR_ALREADY_DIALING,
                      "Another outgoing call is already being dialed.");
          }
          if (mRingingCall.isRinging()) {
              throw new CallStateException(CallStateException.ERROR_CALL_RINGING,
                      "Can't place a call while another is ringing.");
          }
          if (mForegroundCall.getState().isAlive() & mBackgroundCall.getState().isAlive()) {
              throw new CallStateException(CallStateException.ERROR_TOO_MANY_CALLS,
                      "Already an active foreground and background call.");
          }
      }
  
      public boolean
      canTransfer() {
          return mForegroundCall.getState() == ImsPhoneCall.State.ACTIVE
              && mBackgroundCall.getState() == ImsPhoneCall.State.HOLDING;
      }
  
      //***** Private Instance Methods
  
      private void
      internalClearDisconnected() {
          mRingingCall.clearDisconnected();
          mForegroundCall.clearDisconnected();
          mBackgroundCall.clearDisconnected();
          mHandoverCall.clearDisconnected();
      }
  
      @UnsupportedAppUsage
      private void
      updatePhoneState() {
          PhoneConstants.State oldState = mState;
  
          boolean isPendingMOIdle = mPendingMO == null || !mPendingMO.getState().isAlive();
  
          if (mRingingCall.isRinging()) {
              mState = PhoneConstants.State.RINGING;
          } else if (!isPendingMOIdle || !mForegroundCall.isIdle() || !mBackgroundCall.isIdle()) {
              // There is a non-idle call, so we're off the hook.
              mState = PhoneConstants.State.OFFHOOK;
          } else {
              mState = PhoneConstants.State.IDLE;
          }
  
          if (mState == PhoneConstants.State.IDLE && oldState != mState) {
              mVoiceCallEndedRegistrants.notifyRegistrants(
                      new AsyncResult(null, null, null));
          } else if (oldState == PhoneConstants.State.IDLE && oldState != mState) {
              mVoiceCallStartedRegistrants.notifyRegistrants (
                      new AsyncResult(null, null, null));
          }
  
          if (DBG) {
              log("updatePhoneState pendingMo = " + (mPendingMO == null ? "null"
                      : mPendingMO.getState()) + ", fg= " + mForegroundCall.getState() + "("
                      + mForegroundCall.getConnections().size() + "), bg= " + mBackgroundCall
                      .getState() + "(" + mBackgroundCall.getConnections().size() + ")");
              log("updatePhoneState oldState=" + oldState + ", newState=" + mState);
          }
  
          if (mState != oldState) {
              mPhone.notifyPhoneStateChanged();
              mMetrics.writePhoneState(mPhone.getPhoneId(), mState);
              notifyPhoneStateChanged(oldState, mState);
          }
      }
  
      private void
      handleRadioNotAvailable() {
          // handlePollCalls will clear out its
          // call list when it gets the CommandException
          // error result from this
          pollCallsWhenSafe();
      }
  
      private void
      dumpState() {
          List l;
  
          log("Phone State:" + mState);
  
          log("Ringing call: " + mRingingCall.toString());
  
          l = mRingingCall.getConnections();
          for (int i = 0, s = l.size(); i < s; i++) {
              log(l.get(i).toString());
          }
  
          log("Foreground call: " + mForegroundCall.toString());
  
          l = mForegroundCall.getConnections();
          for (int i = 0, s = l.size(); i < s; i++) {
              log(l.get(i).toString());
          }
  
          log("Background call: " + mBackgroundCall.toString());
  
          l = mBackgroundCall.getConnections();
          for (int i = 0, s = l.size(); i < s; i++) {
              log(l.get(i).toString());
          }
  
      }
  
      //***** Called from ImsPhone
      /**
       * Set the TTY mode. This is the actual tty mode (varies depending on peripheral status)
       */
      public void setTtyMode(int ttyMode) {
          if (mImsManager == null) {
              Log.w(LOG_TAG, "ImsManager is null when setting TTY mode");
              return;
          }
  
          try {
              mImsManager.setTtyMode(ttyMode);
          } catch (ImsException e) {
              loge("setTtyMode : " + e);
              retryGetImsService();
          }
      }
  
      /**
       * Sets the UI TTY mode. This is the preferred TTY mode that the user sets in the call
       * settings screen.
       */
      public void setUiTTYMode(int uiTtyMode, Message onComplete) {
          if (mImsManager == null) {
              mPhone.sendErrorResponse(onComplete, getImsManagerIsNullException());
              return;
          }
  
          try {
              mImsManager.setUiTTYMode(mPhone.getContext(), uiTtyMode, onComplete);
          } catch (ImsException e) {
              loge("setUITTYMode : " + e);
              mPhone.sendErrorResponse(onComplete, e);
              retryGetImsService();
          }
      }
  
      public void setMute(boolean mute) {
          mDesiredMute = mute;
          mForegroundCall.setMute(mute);
      }
  
      public boolean getMute() {
          return mDesiredMute;
      }
  
      /**
       * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2833</a>,
       * event 0 ~ 9 maps to decimal value 0 ~ 9, '*' to 10, '#' to 11, event 'A' ~ 'D' to 12 ~ 15,
       * and event flash to 16. Currently, event flash is not supported.
       *
       * @param c that represents the DTMF to send. '0' ~ '9', 'A' ~ 'D', '*', '#' are valid inputs.
       * @param result the result message to send when done. If non-null, the {@link Message} must
       *         contain a valid {@link android.os.Messenger} in the {@link Message#replyTo} field,
       *         since this can be used across IPC boundaries.
       */
      public void sendDtmf(char c, Message result) {
          if (DBG) log("sendDtmf");
  
          ImsCall imscall = mForegroundCall.getImsCall();
          if (imscall != null) {
              imscall.sendDtmf(c, result);
          }
      }
  
      public void
      startDtmf(char c) {
          if (DBG) log("startDtmf");
  
          ImsCall imscall = mForegroundCall.getImsCall();
          if (imscall != null) {
              imscall.startDtmf(c);
          } else {
              loge("startDtmf : no foreground call");
          }
      }
  
      public void
      stopDtmf() {
          if (DBG) log("stopDtmf");
  
          ImsCall imscall = mForegroundCall.getImsCall();
          if (imscall != null) {
              imscall.stopDtmf();
          } else {
              loge("stopDtmf : no foreground call");
          }
      }
  
      //***** Called from ImsPhoneConnection
  
      public void hangup (ImsPhoneConnection conn) throws CallStateException {
          if (DBG) log("hangup connection");
  
          if (conn.getOwner() != this) {
              throw new CallStateException ("ImsPhoneConnection " + conn
                      + "does not belong to ImsPhoneCallTracker " + this);
          }
  
          hangup(conn.getCall());
      }
  
      //***** Called from ImsPhoneCall
  
      public void hangup (ImsPhoneCall call) throws CallStateException {
          if (DBG) log("hangup call");
  
          if (call.getConnections().size() == 0) {
              throw new CallStateException("no connections");
          }
  
          ImsCall imsCall = call.getImsCall();
          boolean rejectCall = false;
  
          if (call == mRingingCall) {
              if (Phone.DEBUG_PHONE) log("(ringing) hangup incoming");
              rejectCall = true;
          } else if (call == mForegroundCall) {
              if (call.isDialingOrAlerting()) {
                  if (Phone.DEBUG_PHONE) {
                      log("(foregnd) hangup dialing or alerting...");
                  }
              } else {
                  if (Phone.DEBUG_PHONE) {
                      log("(foregnd) hangup foreground");
                  }
                  //held call will be resumed by onCallTerminated
              }
          } else if (call == mBackgroundCall) {
              if (Phone.DEBUG_PHONE) {
                  log("(backgnd) hangup waiting or background");
              }
          } else {
              throw new CallStateException ("ImsPhoneCall " + call +
                      "does not belong to ImsPhoneCallTracker " + this);
          }
  
          call.onHangupLocal();
  
          try {
              if (imsCall != null) {
                  if (rejectCall) {
                      imsCall.reject(ImsReasonInfo.CODE_USER_DECLINE);
                      mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
                              ImsCommand.IMS_CMD_REJECT);
                  } else {
                      imsCall.terminate(ImsReasonInfo.CODE_USER_TERMINATED);
                      mMetrics.writeOnImsCommand(mPhone.getPhoneId(), imsCall.getSession(),
                              ImsCommand.IMS_CMD_TERMINATE);
                  }
              } else if (mPendingMO != null && call == mForegroundCall) {
                  // is holding a foreground call
                  mPendingMO.update(null, ImsPhoneCall.State.DISCONNECTED);
                  mPendingMO.onDisconnect();
                  removeConnection(mPendingMO);
                  mPendingMO = null;
                  updatePhoneState();
                  removeMessages(EVENT_DIAL_PENDINGMO);
              }
          } catch (ImsException e) {
              throw new CallStateException(e.getMessage());
          }
  
          mPhone.notifyPreciseCallStateChanged();
      }
  
      void callEndCleanupHandOverCallIfAny() {
          if (mHandoverCall.mConnections.size() > 0) {
              if (DBG) log("callEndCleanupHandOverCallIfAny, mHandoverCall.mConnections="
                      + mHandoverCall.mConnections);
              mHandoverCall.mConnections.clear();
              mConnections.clear();
              mState = PhoneConstants.State.IDLE;
          }
      }
  
  
      public void sendUSSD (String ussdString, Message response) {
          if (DBG) log("sendUSSD");
  
          try {
              if (mUssdSession != null) {
                  // Doesn't need mPendingUssd here. Listeners would use it if not null.
                  mPendingUssd = null;
                  mUssdSession.sendUssd(ussdString);
                  AsyncResult.forMessage(response, null, null);
                  response.sendToTarget();
                  return;
              }
  
              if (mImsManager == null) {
                  mPhone.sendErrorResponse(response, getImsManagerIsNullException());
                  return;
              }
  
              String[] callees = new String[] { ussdString };
              ImsCallProfile profile = mImsManager.createCallProfile(
                      ImsCallProfile.SERVICE_TYPE_NORMAL, ImsCallProfile.CALL_TYPE_VOICE);
              profile.setCallExtraInt(ImsCallProfile.EXTRA_DIALSTRING,
                      ImsCallProfile.DIALSTRING_USSD);
  
              mUssdSession = mImsManager.makeCall(profile, callees, mImsUssdListener);
              mPendingUssd = response;
              if (DBG) log("pending ussd updated, " + mPendingUssd);
          } catch (ImsException e) {
              loge("sendUSSD : " + e);
              mPhone.sendErrorResponse(response, e);
              retryGetImsService();
          }
      }
  
      /**
       * Cancel USSD session.
       *
       * @param msg The message to dispatch when the USSD session terminated.
       */
      public void cancelUSSD(Message msg) {
          if (mUssdSession == null) return;
          mPendingUssd = msg;
          mUssdSession.terminate(ImsReasonInfo.CODE_USER_TERMINATED);
      }
  
      private synchronized ImsPhoneConnection findConnection(final ImsCall imsCall) {
          for (ImsPhoneConnection conn : mConnections) {
              if (conn.getImsCall() == imsCall) {
                  return conn;
              }
          }
          return null;
      }
  
      @UnsupportedAppUsage
      private synchronized void removeConnection(ImsPhoneConnection conn) {
          mConnections.remove(conn);
          // If not emergency call is remaining, notify emergency call registrants
          if (mIsInEmergencyCall) {
              boolean isEmergencyCallInList = false;
              // if no emergency calls pending, set this to false
              for (ImsPhoneConnection imsPhoneConnection : mConnections) {
                  if (imsPhoneConnection != null && imsPhoneConnection.isEmergency() == true) {
                      isEmergencyCallInList = true;
                      break;
                  }
              }
  
              if (!isEmergencyCallInList) {
                  mIsInEmergencyCall = false;
                  mPhone.sendEmergencyCallStateChange(false);
              }
          }
      }
  
      @UnsupportedAppUsage
      private synchronized void addConnection(ImsPhoneConnection conn) {
          mConnections.add(conn);
          if (conn.isEmergency()) {
              mIsInEmergencyCall = true;
              mPhone.sendEmergencyCallStateChange(true);
          }
      }
  
      private void processCallStateChange(ImsCall imsCall, ImsPhoneCall.State state, int cause) {
          if (DBG) log("processCallStateChange " + imsCall + " state=" + state + " cause=" + cause);
          // This method is called on onCallUpdate() where there is not necessarily a call state
          // change. In these situations, we'll ignore the state related updates and only process
          // the change in media capabilities (as expected).  The default is to not ignore state
          // changes so we do not change existing behavior.
          processCallStateChange(imsCall, state, cause, false /* do not ignore state update */);
      }
  
      private void processCallStateChange(ImsCall imsCall, ImsPhoneCall.State state, int cause,
              boolean ignoreState) {
          if (DBG) {
              log("processCallStateChange state=" + state + " cause=" + cause
                      + " ignoreState=" + ignoreState);
          }
  
          if (imsCall == null) return;
  
          boolean changed = false;
          ImsPhoneConnection conn = findConnection(imsCall);
  
          if (conn == null) {
              // TODO : what should be done?
              return;
          }
  
          // processCallStateChange is triggered for onCallUpdated as well.
          // onCallUpdated should not modify the state of the call
          // It should modify only other capabilities of call through updateMediaCapabilities
          // State updates will be triggered through individual callbacks
          // i.e. onCallHeld, onCallResume, etc and conn.update will be responsible for the update
          conn.updateMediaCapabilities(imsCall);
          if (ignoreState) {
              conn.updateAddressDisplay(imsCall);
              conn.updateExtras(imsCall);
  
              maybeSetVideoCallProvider(conn, imsCall);
              return;
          }
  
          changed = conn.update(imsCall, state);
          if (state == ImsPhoneCall.State.DISCONNECTED) {
              changed = conn.onDisconnect(cause) || changed;
              //detach the disconnected connections
              conn.getCall().detach(conn);
              removeConnection(conn);
          }
  
          if (changed) {
              if (conn.getCall() == mHandoverCall) return;
              updatePhoneState();
              mPhone.notifyPreciseCallStateChanged();
          }
      }
  
      private void maybeSetVideoCallProvider(ImsPhoneConnection conn, ImsCall imsCall) {
          android.telecom.Connection.VideoProvider connVideoProvider = conn.getVideoProvider();
          if (connVideoProvider != null || imsCall.getCallSession().getVideoCallProvider() == null) {
              return;
          }
  
          try {
              setVideoCallProvider(conn, imsCall);
          } catch (RemoteException e) {
              loge("maybeSetVideoCallProvider: exception " + e);
          }
      }
  
      /**
       * Adds a reason code remapping, for test purposes.
       *
       * @param fromCode The from code, or {@code null} if all.
       * @param message The message to map.
       * @param toCode The code to remap to.
       */
      @VisibleForTesting
      public void addReasonCodeRemapping(Integer fromCode, String message, Integer toCode) {
          if (message != null) {
              message = message.toLowerCase();
          }
          mImsReasonCodeMap.put(new Pair<>(fromCode, message), toCode);
      }
  
      /**
       * Returns the {@link ImsReasonInfo#getCode()}, potentially remapping to a new value based on
       * the {@link ImsReasonInfo#getCode()} and {@link ImsReasonInfo#getExtraMessage()}.
       *
       * See {@link #mImsReasonCodeMap}.
       *
       * @param reasonInfo The {@link ImsReasonInfo}.
       * @return The remapped code.
       */
      @VisibleForTesting
      public @ImsReasonInfo.ImsCode int maybeRemapReasonCode(ImsReasonInfo reasonInfo) {
          int code = reasonInfo.getCode();
          String reason = reasonInfo.getExtraMessage();
          if (reason == null) {
              reason = "";
          } else {
              reason = reason.toLowerCase();
          }
          log("maybeRemapReasonCode : fromCode = " + reasonInfo.getCode() + " ; message = "
                  + reason);
          Pair<Integer, String> toCheck = new Pair<>(code, reason);
          Pair<Integer, String> wildcardToCheck = new Pair<>(null, reason);
          if (mImsReasonCodeMap.containsKey(toCheck)) {
              int toCode = mImsReasonCodeMap.get(toCheck);
  
              log("maybeRemapReasonCode : fromCode = " + reasonInfo.getCode() + " ; message = "
                      + reason + " ; toCode = " + toCode);
              return toCode;
          } else if (!reason.isEmpty() && mImsReasonCodeMap.containsKey(wildcardToCheck)) {
              // Handle the case where a wildcard is specified for the fromCode; in this case we will
              // match without caring about the fromCode.
              // If the reason is empty, we won't do wildcard remapping; otherwise we'd basically be
              // able to remap all ImsReasonInfo codes to a single code, which is not desirable.
              int toCode = mImsReasonCodeMap.get(wildcardToCheck);
  
              log("maybeRemapReasonCode : fromCode(wildcard) = " + reasonInfo.getCode() +
                      " ; message = " + reason + " ; toCode = " + toCode);
              return toCode;
          }
          return code;
      }
  
      /**
       * Maps an {@link ImsReasonInfo} reason code to a {@link DisconnectCause} cause code.
       * The {@link Call.State} provided is the state of the call prior to disconnection.
       * @param reasonInfo the {@link ImsReasonInfo} for the disconnection.
       * @param callState The {@link Call.State} prior to disconnection.
       * @return The {@link DisconnectCause} code.
       */
      @VisibleForTesting
      public int getDisconnectCauseFromReasonInfo(ImsReasonInfo reasonInfo, Call.State callState) {
          int cause = DisconnectCause.ERROR_UNSPECIFIED;
  
          int code = maybeRemapReasonCode(reasonInfo);
          switch (code) {
              case ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL:
                  return DisconnectCause.IMS_SIP_ALTERNATE_EMERGENCY_CALL;
              case ImsReasonInfo.CODE_SIP_BAD_ADDRESS:
              case ImsReasonInfo.CODE_SIP_NOT_REACHABLE:
                  return DisconnectCause.NUMBER_UNREACHABLE;
  
              case ImsReasonInfo.CODE_SIP_BUSY:
                  return DisconnectCause.BUSY;
  
              case ImsReasonInfo.CODE_USER_TERMINATED:
                  return DisconnectCause.LOCAL;
  
              case ImsReasonInfo.CODE_LOCAL_ENDED_BY_CONFERENCE_MERGE:
                  return DisconnectCause.IMS_MERGED_SUCCESSFULLY;
  
              case ImsReasonInfo.CODE_LOCAL_CALL_DECLINE:
              case ImsReasonInfo.CODE_REMOTE_CALL_DECLINE:
                  // If the call has been declined locally (on this device), or on remotely (on
                  // another device using multiendpoint functionality), mark it as rejected.
                  return DisconnectCause.INCOMING_REJECTED;
  
              case ImsReasonInfo.CODE_USER_TERMINATED_BY_REMOTE:
              case ImsReasonInfo.CODE_SIP_USER_REJECTED:
                  return DisconnectCause.NORMAL;
  
              case ImsReasonInfo.CODE_SIP_FORBIDDEN:
                  return DisconnectCause.SERVER_ERROR;
  
              case ImsReasonInfo.CODE_SIP_REDIRECTED:
              case ImsReasonInfo.CODE_SIP_BAD_REQUEST:
              case ImsReasonInfo.CODE_SIP_NOT_ACCEPTABLE:
              case ImsReasonInfo.CODE_SIP_GLOBAL_ERROR:
                  return DisconnectCause.SERVER_ERROR;
  
              case ImsReasonInfo.CODE_EMERGENCY_CALL_OVER_WFC_NOT_AVAILABLE:
                  return DisconnectCause.EMERGENCY_CALL_OVER_WFC_NOT_AVAILABLE;
  
              case ImsReasonInfo.CODE_WFC_SERVICE_NOT_AVAILABLE_IN_THIS_LOCATION:
                  return DisconnectCause.WFC_SERVICE_NOT_AVAILABLE_IN_THIS_LOCATION;
  
              case ImsReasonInfo.CODE_SIP_SERVICE_UNAVAILABLE:
              case ImsReasonInfo.CODE_SIP_SERVER_ERROR:
                  return DisconnectCause.SERVER_UNREACHABLE;
  
              case ImsReasonInfo.CODE_SIP_NOT_FOUND:
                  return DisconnectCause.INVALID_NUMBER;
  
              case ImsReasonInfo.CODE_LOCAL_NETWORK_ROAMING:
              case ImsReasonInfo.CODE_LOCAL_NETWORK_IP_CHANGED:
              case ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN:
              case ImsReasonInfo.CODE_LOCAL_SERVICE_UNAVAILABLE:
              case ImsReasonInfo.CODE_LOCAL_NOT_REGISTERED:
              case ImsReasonInfo.CODE_LOCAL_NETWORK_NO_LTE_COVERAGE:
              case ImsReasonInfo.CODE_LOCAL_NETWORK_NO_SERVICE:
              case ImsReasonInfo.CODE_LOCAL_CALL_VCC_ON_PROGRESSING:
                  return DisconnectCause.OUT_OF_SERVICE;
  
              case ImsReasonInfo.CODE_SIP_REQUEST_TIMEOUT:
              case ImsReasonInfo.CODE_TIMEOUT_1XX_WAITING:
              case ImsReasonInfo.CODE_TIMEOUT_NO_ANSWER:
              case ImsReasonInfo.CODE_TIMEOUT_NO_ANSWER_CALL_UPDATE:
                  return DisconnectCause.TIMED_OUT;
  
              case ImsReasonInfo.CODE_LOCAL_POWER_OFF:
                  return DisconnectCause.POWER_OFF;
  
              case ImsReasonInfo.CODE_LOCAL_LOW_BATTERY:
              case ImsReasonInfo.CODE_LOW_BATTERY: {
                  if (callState == Call.State.DIALING) {
                      return DisconnectCause.DIAL_LOW_BATTERY;
                  } else {
                      return DisconnectCause.LOW_BATTERY;
                  }
              }
  
              case ImsReasonInfo.CODE_CALL_BARRED:
                  return DisconnectCause.CALL_BARRED;
  
              case ImsReasonInfo.CODE_FDN_BLOCKED:
                  return DisconnectCause.FDN_BLOCKED;
  
              case ImsReasonInfo.CODE_IMEI_NOT_ACCEPTED:
                  return DisconnectCause.IMEI_NOT_ACCEPTED;
  
              case ImsReasonInfo.CODE_ANSWERED_ELSEWHERE:
                  return DisconnectCause.ANSWERED_ELSEWHERE;
  
              case ImsReasonInfo.CODE_CALL_END_CAUSE_CALL_PULL:
                  return DisconnectCause.CALL_PULLED;
  
              case ImsReasonInfo.CODE_MAXIMUM_NUMBER_OF_CALLS_REACHED:
                  return DisconnectCause.MAXIMUM_NUMBER_OF_CALLS_REACHED;
  
              case ImsReasonInfo.CODE_DATA_DISABLED:
                  return DisconnectCause.DATA_DISABLED;
  
              case ImsReasonInfo.CODE_DATA_LIMIT_REACHED:
                  return DisconnectCause.DATA_LIMIT_REACHED;
  
              case ImsReasonInfo.CODE_WIFI_LOST:
                  return DisconnectCause.WIFI_LOST;
  
              case ImsReasonInfo.CODE_ACCESS_CLASS_BLOCKED:
                  return DisconnectCause.IMS_ACCESS_BLOCKED;
  
              case ImsReasonInfo.CODE_EMERGENCY_TEMP_FAILURE:
                  return DisconnectCause.EMERGENCY_TEMP_FAILURE;
  
              case ImsReasonInfo.CODE_EMERGENCY_PERM_FAILURE:
                  return DisconnectCause.EMERGENCY_PERM_FAILURE;
  
              case ImsReasonInfo.CODE_DIAL_MODIFIED_TO_USSD:
                  return DisconnectCause.DIAL_MODIFIED_TO_USSD;
  
              case ImsReasonInfo.CODE_DIAL_MODIFIED_TO_SS:
                  return DisconnectCause.DIAL_MODIFIED_TO_SS;
  
              case ImsReasonInfo.CODE_DIAL_MODIFIED_TO_DIAL:
                  return DisconnectCause.DIAL_MODIFIED_TO_DIAL;
  
              case ImsReasonInfo.CODE_DIAL_MODIFIED_TO_DIAL_VIDEO:
                  return DisconnectCause.DIAL_MODIFIED_TO_DIAL_VIDEO;
  
              case ImsReasonInfo.CODE_DIAL_VIDEO_MODIFIED_TO_DIAL:
                  return DisconnectCause.DIAL_VIDEO_MODIFIED_TO_DIAL;
  
              case ImsReasonInfo.CODE_DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO:
                  return DisconnectCause.DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO;
  
              case ImsReasonInfo.CODE_DIAL_VIDEO_MODIFIED_TO_SS:
                  return DisconnectCause.DIAL_VIDEO_MODIFIED_TO_SS;
  
              case ImsReasonInfo.CODE_DIAL_VIDEO_MODIFIED_TO_USSD:
                  return DisconnectCause.DIAL_VIDEO_MODIFIED_TO_USSD;
  
              case ImsReasonInfo.CODE_UNOBTAINABLE_NUMBER:
                  return DisconnectCause.UNOBTAINABLE_NUMBER;
  
              case ImsReasonInfo.CODE_UNSPECIFIED:
                  if (mPhone.getDefaultPhone().getServiceStateTracker().mRestrictedState
                          .isCsRestricted()) {
                      return DisconnectCause.CS_RESTRICTED;
                  } else if (mPhone.getDefaultPhone().getServiceStateTracker().mRestrictedState
                          .isCsEmergencyRestricted()) {
                      return DisconnectCause.CS_RESTRICTED_EMERGENCY;
                  } else if (mPhone.getDefaultPhone().getServiceStateTracker().mRestrictedState
                          .isCsNormalRestricted()) {
                      return DisconnectCause.CS_RESTRICTED_NORMAL;
                  }
                  break;
  
              default:
          }
  
          return cause;
      }
  
      /**
       * @return true if the phone is in Emergency Callback mode, otherwise false
       */
      private boolean isPhoneInEcbMode() {
          return mPhone != null && mPhone.isInEcm();
      }
  
      /**
       * Before dialing pending MO request, check for the Emergency Callback mode.
       * If device is in Emergency callback mode, then exit the mode before dialing pending MO.
       */
      @UnsupportedAppUsage
      private void dialPendingMO() {
          boolean isPhoneInEcmMode = isPhoneInEcbMode();
          boolean isEmergencyNumber = mPendingMO.isEmergency();
          if ((!isPhoneInEcmMode) || (isPhoneInEcmMode && isEmergencyNumber)) {
              sendEmptyMessage(EVENT_DIAL_PENDINGMO);
          } else {
              sendEmptyMessage(EVENT_EXIT_ECBM_BEFORE_PENDINGMO);
          }
      }
  
      /**
       * Listen to the IMS call state change
       */
      private ImsCall.Listener mImsCallListener = new ImsCall.Listener() {
          @Override
          public void onCallProgressing(ImsCall imsCall) {
              if (DBG) log("onCallProgressing");
  
              mPendingMO = null;
              processCallStateChange(imsCall, ImsPhoneCall.State.ALERTING,
                      DisconnectCause.NOT_DISCONNECTED);
              mMetrics.writeOnImsCallProgressing(mPhone.getPhoneId(), imsCall.getCallSession());
          }
  
          @Override
          public void onCallStarted(ImsCall imsCall) {
              if (DBG) log("onCallStarted");
  
              if (mHoldSwitchingState == HoldSwapState.HOLDING_TO_ANSWER_INCOMING) {
                  // If we put a call on hold to answer an incoming call, we should reset the
                  // variables that keep track of the switch here.
                  if (mCallExpectedToResume != null && mCallExpectedToResume == imsCall) {
                      if (DBG) log("onCallStarted: starting a call as a result of a switch.");
                      mHoldSwitchingState = HoldSwapState.INACTIVE;
                      mCallExpectedToResume = null;
                      logHoldSwapState("onCallStarted");
                  }
              }
  
              mPendingMO = null;
              processCallStateChange(imsCall, ImsPhoneCall.State.ACTIVE,
                      DisconnectCause.NOT_DISCONNECTED);
  
              if (mNotifyVtHandoverToWifiFail && imsCall.isVideoCall() && !imsCall.isWifiCall()) {
                  if (isWifiConnected()) {
                      // Schedule check to see if handover succeeded.
                      sendMessageDelayed(obtainMessage(EVENT_CHECK_FOR_WIFI_HANDOVER, imsCall),
                              HANDOVER_TO_WIFI_TIMEOUT_MS);
                      mHasAttemptedStartOfCallHandover = false;
                  } else {
                      // No wifi connectivity, so keep track of network availability for potential
                      // handover.
                      registerForConnectivityChanges();
                      // No WIFI, so assume we've already attempted a handover.
                      mHasAttemptedStartOfCallHandover = true;
                  }
              }
              mMetrics.writeOnImsCallStarted(mPhone.getPhoneId(), imsCall.getCallSession());
          }
  
          @Override
          public void onCallUpdated(ImsCall imsCall) {
              if (DBG) log("onCallUpdated");
              if (imsCall == null) {
                  return;
              }
              ImsPhoneConnection conn = findConnection(imsCall);
              if (conn != null) {
                  if (DBG) log("onCallUpdated: profile is " + imsCall.getCallProfile());
                  processCallStateChange(imsCall, conn.getCall().mState,
                          DisconnectCause.NOT_DISCONNECTED, true /*ignore state update*/);
                  mMetrics.writeImsCallState(mPhone.getPhoneId(),
                          imsCall.getCallSession(), conn.getCall().mState);
              }
          }
  
          /**
           * onCallStartFailed will be invoked when:
           * case 1) Dialing fails
           * case 2) Ringing call is disconnected by local or remote user
           */
          @Override
          public void onCallStartFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) {
              if (DBG) log("onCallStartFailed reasonCode=" + reasonInfo.getCode());
  
              if (mHoldSwitchingState == HoldSwapState.HOLDING_TO_ANSWER_INCOMING) {
                  // If we put a call on hold to answer an incoming call, we should reset the
                  // variables that keep track of the switch here.
                  if (mCallExpectedToResume != null && mCallExpectedToResume == imsCall) {
                      if (DBG) log("onCallStarted: starting a call as a result of a switch.");
                      mHoldSwitchingState = HoldSwapState.INACTIVE;
                      mCallExpectedToResume = null;
                      logHoldSwapState("onCallStartFailed");
                  }
              }
  
              if (mPendingMO != null) {
                  // To initiate dialing circuit-switched call
                  if (reasonInfo.getCode() == ImsReasonInfo.CODE_LOCAL_CALL_CS_RETRY_REQUIRED
                          && mBackgroundCall.getState() == ImsPhoneCall.State.IDLE
                          && mRingingCall.getState() == ImsPhoneCall.State.IDLE) {
                      mForegroundCall.detach(mPendingMO);
                      removeConnection(mPendingMO);
                      mPendingMO.finalize();
                      mPendingMO = null;
                      mPhone.initiateSilentRedial();
                      return;
                  } else {
                      sendCallStartFailedDisconnect(imsCall, reasonInfo);
                  }
                  mMetrics.writeOnImsCallStartFailed(mPhone.getPhoneId(), imsCall.getCallSession(),
                          reasonInfo);
              }
          }
  
          @Override
          public void onCallTerminated(ImsCall imsCall, ImsReasonInfo reasonInfo) {
              if (DBG) log("onCallTerminated reasonCode=" + reasonInfo.getCode());
  
              ImsPhoneConnection conn = findConnection(imsCall);
              Call.State callState;
              if (conn != null) {
                  callState = conn.getState();
              } else {
                  // Connection shouldn't be null, but if it is, we can assume the call was active.
                  // This call state is only used for determining which disconnect message to show in
                  // the case of the device's battery being low resulting in a call drop.
                  callState = Call.State.ACTIVE;
              }
              int cause = getDisconnectCauseFromReasonInfo(reasonInfo, callState);
  
              if (DBG) log("cause = " + cause + " conn = " + conn);
  
              if (conn != null) {
                  android.telecom.Connection.VideoProvider videoProvider = conn.getVideoProvider();
                  if (videoProvider instanceof ImsVideoCallProviderWrapper) {
                      ImsVideoCallProviderWrapper wrapper = (ImsVideoCallProviderWrapper)
                              videoProvider;
                      wrapper.unregisterForDataUsageUpdate(ImsPhoneCallTracker.this);
                      wrapper.removeImsVideoProviderCallback(conn);
                  }
              }
              if (mOnHoldToneId == System.identityHashCode(conn)) {
                  if (conn != null && mOnHoldToneStarted) {
                      mPhone.stopOnHoldTone(conn);
                  }
                  mOnHoldToneStarted = false;
                  mOnHoldToneId = -1;
              }
              if (conn != null) {
                  if (conn.isPulledCall() && (
                          reasonInfo.getCode() == ImsReasonInfo.CODE_CALL_PULL_OUT_OF_SYNC ||
                          reasonInfo.getCode() == ImsReasonInfo.CODE_SIP_TEMPRARILY_UNAVAILABLE ||
                          reasonInfo.getCode() == ImsReasonInfo.CODE_SIP_FORBIDDEN) &&
                          mPhone != null && mPhone.getExternalCallTracker() != null) {
  
                      log("Call pull failed.");
                      // Call was being pulled, but the call pull has failed -- inform the associated
                      // TelephonyConnection that the pull failed, and provide it with the original
                      // external connection which was pulled so that it can be swapped back.
                      conn.onCallPullFailed(mPhone.getExternalCallTracker()
                              .getConnectionById(conn.getPulledDialogId()));
                      // Do not mark as disconnected; the call will just change from being a regular
                      // call to being an external call again.
                      cause = DisconnectCause.NOT_DISCONNECTED;
  
                  } else if (conn.isIncoming() && conn.getConnectTime() == 0
                          && cause != DisconnectCause.ANSWERED_ELSEWHERE) {
                      // Missed
                      if (cause == DisconnectCause.NORMAL) {
                          cause = DisconnectCause.INCOMING_MISSED;
                      } else {
                          cause = DisconnectCause.INCOMING_REJECTED;
                      }
                      if (DBG) log("Incoming connection of 0 connect time detected - translated " +
                              "cause = " + cause);
                  }
              }
  
              if (cause == DisconnectCause.NORMAL && conn != null && conn.getImsCall().isMerged()) {
                  // Call was terminated while it is merged instead of a remote disconnect.
                  cause = DisconnectCause.IMS_MERGED_SUCCESSFULLY;
              }
  
              String callId = imsCall.getSession().getCallId();
              mMetrics.writeOnImsCallTerminated(mPhone.getPhoneId(), imsCall.getCallSession(),
                      reasonInfo, mCallQualityMetrics.get(callId), conn.getEmergencyNumberInfo(),
                      getNetworkCountryIso());
              // Remove info for the callId from the current calls and add it to the history
              CallQualityMetrics lastCallMetrics = mCallQualityMetrics.remove(callId);
              if (lastCallMetrics != null) {
                  mCallQualityMetricsHistory.add(lastCallMetrics);
              }
              pruneCallQualityMetricsHistory();
              mPhone.notifyImsReason(reasonInfo);
  
              if (reasonInfo.getCode() == ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL
                      && mAutoRetryFailedWifiEmergencyCall) {
                  Pair<ImsCall, ImsReasonInfo> callInfo = new Pair<>(imsCall, reasonInfo);
                  mPhone.getDefaultPhone().getServiceStateTracker().registerForNetworkAttached(
                          ImsPhoneCallTracker.this, EVENT_REDIAL_WIFI_E911_CALL, callInfo);
                  sendMessageDelayed(obtainMessage(EVENT_REDIAL_WIFI_E911_TIMEOUT, callInfo),
                          TIMEOUT_REDIAL_WIFI_E911_MS);
                  final ConnectivityManager mgr = (ConnectivityManager) mPhone.getContext()
                          .getSystemService(Context.CONNECTIVITY_SERVICE);
                  mgr.setAirplaneMode(false);
                  return;
              } else if (reasonInfo.getCode() == ImsReasonInfo.CODE_RETRY_ON_IMS_WITHOUT_RTT) {
                  Pair<ImsCall, ImsReasonInfo> callInfo = new Pair<>(imsCall, reasonInfo);
                  sendMessage(obtainMessage(EVENT_REDIAL_WITHOUT_RTT, callInfo));
                  return;
              } else {
                  processCallStateChange(imsCall, ImsPhoneCall.State.DISCONNECTED, cause);
              }
  
              if (mForegroundCall.getState() != ImsPhoneCall.State.ACTIVE) {
                  if (mRingingCall.getState().isRinging()) {
                      // Drop pending MO. We should address incoming call first
                      mPendingMO = null;
                  }
              }
  
              if (mHoldSwitchingState == HoldSwapState.SWAPPING_ACTIVE_AND_HELD) {
                  if (DBG) {
                      log("onCallTerminated: Call terminated in the midst of Switching " +
                              "Fg and Bg calls.");
                  }
                  // If we are the in midst of swapping FG and BG calls and the call that was
                  // terminated was the one that we expected to resume, we need to swap the FG and
                  // BG calls back.
                  if (imsCall == mCallExpectedToResume) {
                      if (DBG) {
                          log("onCallTerminated: switching " + mForegroundCall + " with "
                                  + mBackgroundCall);
                      }
                      mForegroundCall.switchWith(mBackgroundCall);
                  }
                  // This call terminated in the midst of a switch after the other call was held, so
                  // resume it back to ACTIVE state since the switch failed.
                  log("onCallTerminated: foreground call in state " + mForegroundCall.getState() +
                          " and ringing call in state " + (mRingingCall == null ? "null" :
                          mRingingCall.getState().toString()));
  
                  sendEmptyMessage(EVENT_RESUME_NOW_FOREGROUND_CALL);
                  mHoldSwitchingState = HoldSwapState.INACTIVE;
                  mCallExpectedToResume = null;
                  logHoldSwapState("onCallTerminated swap active and hold case");
              } else if (mHoldSwitchingState == HoldSwapState.PENDING_SINGLE_CALL_UNHOLD
                      || mHoldSwitchingState == HoldSwapState.PENDING_SINGLE_CALL_HOLD) {
                  mCallExpectedToResume = null;
                  mHoldSwitchingState = HoldSwapState.INACTIVE;
                  logHoldSwapState("onCallTerminated single call case");
              } else if (mHoldSwitchingState == HoldSwapState.HOLDING_TO_ANSWER_INCOMING) {
                  // Check to see which call got terminated. If it's the one that was gonna get held,
                  // ignore it. If it's the one that was gonna get answered, restore the one that
                  // possibly got held.
                  if (imsCall == mCallExpectedToResume) {
                      mForegroundCall.switchWith(mBackgroundCall);
                      mCallExpectedToResume = null;
                      mHoldSwitchingState = HoldSwapState.INACTIVE;
                      logHoldSwapState("onCallTerminated hold to answer case");
                      sendEmptyMessage(EVENT_RESUME_NOW_FOREGROUND_CALL);
                  }
              } else if (mHoldSwitchingState == HoldSwapState.HOLDING_TO_DIAL_OUTGOING) {
                  // The call that we were gonna hold might've gotten terminated. If that's the case,
                  // dial mPendingMo if present.
                  if (mPendingMO == null
                          || mPendingMO.getDisconnectCause() != DisconnectCause.NOT_DISCONNECTED) {
                      mHoldSwitchingState = HoldSwapState.INACTIVE;
                      logHoldSwapState("onCallTerminated hold to dial but no pendingMo");
                  } else if (imsCall != mPendingMO.getImsCall()) {
                      sendEmptyMessage(EVENT_DIAL_PENDINGMO);
                      mHoldSwitchingState = HoldSwapState.INACTIVE;
                      logHoldSwapState("onCallTerminated hold to dial, dial pendingMo");
                  }
              }
  
              if (mShouldUpdateImsConfigOnDisconnect) {
                  // Ensure we update the IMS config when the call is disconnected; we delayed this
                  // because a video call was paused.
                  if (mImsManager != null) {
                      mImsManager.updateImsServiceConfig(true);
                  }
                  mShouldUpdateImsConfigOnDisconnect = false;
              }
          }
  
          @Override
          public void onCallHeld(ImsCall imsCall) {
              if (DBG) {
                  if (mForegroundCall.getImsCall() == imsCall) {
                      log("onCallHeld (fg) " + imsCall);
                  } else if (mBackgroundCall.getImsCall() == imsCall) {
                      log("onCallHeld (bg) " + imsCall);
                  }
              }
  
              synchronized (mSyncHold) {
                  ImsPhoneCall.State oldState = mBackgroundCall.getState();
                  processCallStateChange(imsCall, ImsPhoneCall.State.HOLDING,
                          DisconnectCause.NOT_DISCONNECTED);
  
                  // Note: If we're performing a switchWaitingOrHoldingAndActive, the call to
                  // processCallStateChange above may have caused the mBackgroundCall and
                  // mForegroundCall references below to change meaning.  Watch out for this if you
                  // are reading through this code.
                  if (oldState == ImsPhoneCall.State.ACTIVE) {
                      // Note: This case comes up when we have just held a call in response to a
                      // switchWaitingOrHoldingAndActive.  We now need to resume the background call.
                      if (mForegroundCall.getState() == ImsPhoneCall.State.HOLDING
                              && mHoldSwitchingState == HoldSwapState.SWAPPING_ACTIVE_AND_HELD) {
                          sendEmptyMessage(EVENT_RESUME_NOW_FOREGROUND_CALL);
                      } else if (mRingingCall.getState() == ImsPhoneCall.State.WAITING
                              && mHoldSwitchingState == HoldSwapState.HOLDING_TO_ANSWER_INCOMING) {
                          sendEmptyMessage(EVENT_ANSWER_WAITING_CALL);
                      } else if (mPendingMO != null
                              && mHoldSwitchingState == HoldSwapState.HOLDING_TO_DIAL_OUTGOING) {
                          dialPendingMO();
                          mHoldSwitchingState = HoldSwapState.INACTIVE;
                          logHoldSwapState("onCallHeld hold to dial");
                      } else {
                          // In this case there will be no call resumed, so we can assume that we
                          // are done switching fg and bg calls now.
                          // This may happen if there is no BG call and we are holding a call so that
                          // we can dial another one.
                          mHoldSwitchingState = HoldSwapState.INACTIVE;
                          logHoldSwapState("onCallHeld normal case");
                      }
                  } else if (oldState == ImsPhoneCall.State.IDLE
                          && (mHoldSwitchingState == HoldSwapState.SWAPPING_ACTIVE_AND_HELD
                                  || mHoldSwitchingState
                                  == HoldSwapState.HOLDING_TO_ANSWER_INCOMING)) {
                      // The other call terminated in the midst of a switch before this call was held,
                      // so resume the foreground call back to ACTIVE state since the switch failed.
                      if (mForegroundCall.getState() == ImsPhoneCall.State.HOLDING) {
                          sendEmptyMessage(EVENT_RESUME_NOW_FOREGROUND_CALL);
                          mHoldSwitchingState = HoldSwapState.INACTIVE;
                          mCallExpectedToResume = null;
                          logHoldSwapState("onCallHeld premature termination of other call");
                      }
                  }
              }
              mMetrics.writeOnImsCallHeld(mPhone.getPhoneId(), imsCall.getCallSession());
          }
  
          @Override
          public void onCallHoldFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) {
              if (DBG) log("onCallHoldFailed reasonCode=" + reasonInfo.getCode());
  
              synchronized (mSyncHold) {
                  ImsPhoneCall.State bgState = mBackgroundCall.getState();
                  if (reasonInfo.getCode() == ImsReasonInfo.CODE_LOCAL_CALL_TERMINATED) {
                      // disconnected while processing hold
                      if (mPendingMO != null) {
                          dialPendingMO();
                      } else if (mRingingCall.getState() == ImsPhoneCall.State.WAITING
                              && mHoldSwitchingState == HoldSwapState.HOLDING_TO_ANSWER_INCOMING) {
                          sendEmptyMessage(EVENT_ANSWER_WAITING_CALL);
                      }
                      mHoldSwitchingState = HoldSwapState.INACTIVE;
                  } else if (mPendingMO != null && mPendingMO.isEmergency()) {
                      // If mPendingMO is an emergency call, disconnect the call that we tried to
                      // hold.
                      mBackgroundCall.getImsCall().terminate(ImsReasonInfo.CODE_UNSPECIFIED);
                      if (imsCall != mCallExpectedToResume) {
                          mCallExpectedToResume = null;
                      }
                      // Leave mHoldSwitchingState as is for now -- we'll reset it
                      // in onCallTerminated, which will also dial the outgoing emergency call.
                  } else if (mRingingCall.getState() == ImsPhoneCall.State.WAITING
                          && mHoldSwitchingState == HoldSwapState.HOLDING_TO_ANSWER_INCOMING) {
                      // If we issued a hold request in order to answer an incoming call, we need
                      // to tell Telecom that we can't actually answer the incoming call.
                      mHoldSwitchingState = HoldSwapState.INACTIVE;
                      mForegroundCall.switchWith(mBackgroundCall);
                      logHoldSwapState("onCallHoldFailed unable to answer waiting call");
                  } else if (bgState == ImsPhoneCall.State.ACTIVE) {
                      mForegroundCall.switchWith(mBackgroundCall);
  
                      if (mPendingMO != null) {
                          mPendingMO.setDisconnectCause(DisconnectCause.ERROR_UNSPECIFIED);
                          sendEmptyMessageDelayed(EVENT_HANGUP_PENDINGMO, TIMEOUT_HANGUP_PENDINGMO);
                      }
                      if (imsCall != mCallExpectedToResume) {
                          mCallExpectedToResume = null;
                      }
                      mHoldSwitchingState = HoldSwapState.INACTIVE;
                  }
                  ImsPhoneConnection conn = findConnection(imsCall);
                  if (conn != null) {
                      conn.onConnectionEvent(android.telecom.Connection.EVENT_CALL_HOLD_FAILED, null);
                  }
                  mPhone.notifySuppServiceFailed(Phone.SuppService.HOLD);
              }
              mMetrics.writeOnImsCallHoldFailed(mPhone.getPhoneId(), imsCall.getCallSession(),
                      reasonInfo);
          }
  
          @Override
          public void onCallResumed(ImsCall imsCall) {
              if (DBG) log("onCallResumed");
  
              // If we are the in midst of swapping FG and BG calls and the call we end up resuming
              // is not the one we expected, we likely had a resume failure and we need to swap the
              // FG and BG calls back.
              if (mHoldSwitchingState == HoldSwapState.SWAPPING_ACTIVE_AND_HELD
                      || mHoldSwitchingState == HoldSwapState.PENDING_RESUME_FOREGROUND_AFTER_FAILURE
                      || mHoldSwitchingState == HoldSwapState.PENDING_SINGLE_CALL_UNHOLD) {
                  if (imsCall != mCallExpectedToResume) {
                      // If the call which resumed isn't as expected, we need to swap back to the
                      // previous configuration; the swap has failed.
                      if (DBG) {
                          log("onCallResumed : switching " + mForegroundCall + " with "
                                  + mBackgroundCall);
                      }
                      mForegroundCall.switchWith(mBackgroundCall);
                  } else {
                      // The call which resumed is the one we expected to resume, so we can clear out
                      // the mSwitchingFgAndBgCalls flag.
                      if (DBG) {
                          log("onCallResumed : expected call resumed.");
                      }
                  }
                  mHoldSwitchingState = HoldSwapState.INACTIVE;
                  mCallExpectedToResume = null;
                  logHoldSwapState("onCallResumed");
              }
              processCallStateChange(imsCall, ImsPhoneCall.State.ACTIVE,
                      DisconnectCause.NOT_DISCONNECTED);
              mMetrics.writeOnImsCallResumed(mPhone.getPhoneId(), imsCall.getCallSession());
          }
  
          @Override
          public void onCallResumeFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) {
              if (mHoldSwitchingState == HoldSwapState.SWAPPING_ACTIVE_AND_HELD
                      || mHoldSwitchingState
                      == HoldSwapState.PENDING_RESUME_FOREGROUND_AFTER_FAILURE) {
                  // If we are in the midst of swapping the FG and BG calls and
                  // we got a resume fail, we need to swap back the FG and BG calls.
                  // Since the FG call was held, will also try to resume the same.
                  if (imsCall == mCallExpectedToResume) {
                      if (DBG) {
                          log("onCallResumeFailed : switching " + mForegroundCall + " with "
                                  + mBackgroundCall);
                      }
                      mForegroundCall.switchWith(mBackgroundCall);
                      if (mForegroundCall.getState() == ImsPhoneCall.State.HOLDING) {
                          sendEmptyMessage(EVENT_RESUME_NOW_FOREGROUND_CALL);
                      }
                  }
  
                  //Call swap is done, reset the relevant variables
                  mCallExpectedToResume = null;
                  mHoldSwitchingState = HoldSwapState.INACTIVE;
                  logHoldSwapState("onCallResumeFailed: multi calls");
              } else if (mHoldSwitchingState == HoldSwapState.PENDING_SINGLE_CALL_UNHOLD) {
                  if (imsCall == mCallExpectedToResume) {
                      if (DBG) {
                          log("onCallResumeFailed: single call unhold case");
                      }
                      mForegroundCall.switchWith(mBackgroundCall);
  
                      mCallExpectedToResume = null;
                      mHoldSwitchingState = HoldSwapState.INACTIVE;
                      logHoldSwapState("onCallResumeFailed: single call");
                  } else {
                      Rlog.w(LOG_TAG, "onCallResumeFailed: got a resume failed for a different call"
                              + " in the single call unhold case");
                  }
              }
              mPhone.notifySuppServiceFailed(Phone.SuppService.RESUME);
              mMetrics.writeOnImsCallResumeFailed(mPhone.getPhoneId(), imsCall.getCallSession(),
                      reasonInfo);
          }
  
          @Override
          public void onCallResumeReceived(ImsCall imsCall) {
              if (DBG) log("onCallResumeReceived");
              ImsPhoneConnection conn = findConnection(imsCall);
              if (conn != null) {
                  if (mOnHoldToneStarted) {
                      mPhone.stopOnHoldTone(conn);
                      mOnHoldToneStarted = false;
                  }
                  conn.onConnectionEvent(android.telecom.Connection.EVENT_CALL_REMOTELY_UNHELD, null);
              }
  
              boolean useVideoPauseWorkaround = mPhone.getContext().getResources().getBoolean(
                      com.android.internal.R.bool.config_useVideoPauseWorkaround);
              if (useVideoPauseWorkaround && mSupportPauseVideo &&
                      VideoProfile.isVideo(conn.getVideoState())) {
                  // If we are using the video pause workaround, the vendor IMS code has issues
                  // with video pause signalling.  In this case, when a call is remotely
                  // held, the modem does not reliably change the video state of the call to be
                  // paused.
                  // As a workaround, we will turn on that bit now.
                  conn.changeToUnPausedState();
              }
  
              SuppServiceNotification supp = new SuppServiceNotification();
              // Type of notification: 0 = MO; 1 = MT
              // Refer SuppServiceNotification class documentation.
              supp.notificationType = 1;
              supp.code = SuppServiceNotification.CODE_2_CALL_RETRIEVED;
              mPhone.notifySuppSvcNotification(supp);
              mMetrics.writeOnImsCallResumeReceived(mPhone.getPhoneId(), imsCall.getCallSession());
          }
  
          @Override
          public void onCallHoldReceived(ImsCall imsCall) {
              ImsPhoneCallTracker.this.onCallHoldReceived(imsCall);
          }
  
          @Override
          public void onCallSuppServiceReceived(ImsCall call,
                  ImsSuppServiceNotification suppServiceInfo) {
              if (DBG) log("onCallSuppServiceReceived: suppServiceInfo=" + suppServiceInfo);
  
              SuppServiceNotification supp = new SuppServiceNotification();
              supp.notificationType = suppServiceInfo.notificationType;
              supp.code = suppServiceInfo.code;
              supp.index = suppServiceInfo.index;
              supp.number = suppServiceInfo.number;
              supp.history = suppServiceInfo.history;
  
              mPhone.notifySuppSvcNotification(supp);
          }
  
          @Override
          public void onCallMerged(final ImsCall call, final ImsCall peerCall, boolean swapCalls) {
              if (DBG) log("onCallMerged");
  
              ImsPhoneCall foregroundImsPhoneCall = findConnection(call).getCall();
              ImsPhoneConnection peerConnection = findConnection(peerCall);
              ImsPhoneCall peerImsPhoneCall = peerConnection == null ? null
                      : peerConnection.getCall();
  
              if (swapCalls) {
                  switchAfterConferenceSuccess();
              }
              foregroundImsPhoneCall.merge(peerImsPhoneCall, ImsPhoneCall.State.ACTIVE);
  
              try {
                  final ImsPhoneConnection conn = findConnection(call);
                  log("onCallMerged: ImsPhoneConnection=" + conn);
                  log("onCallMerged: CurrentVideoProvider=" + conn.getVideoProvider());
                  setVideoCallProvider(conn, call);
                  log("onCallMerged: CurrentVideoProvider=" + conn.getVideoProvider());
              } catch (Exception e) {
                  loge("onCallMerged: exception " + e);
              }
  
              // After merge complete, update foreground as Active
              // and background call as Held, if background call exists
              processCallStateChange(mForegroundCall.getImsCall(), ImsPhoneCall.State.ACTIVE,
                      DisconnectCause.NOT_DISCONNECTED);
              if (peerConnection != null) {
                  processCallStateChange(mBackgroundCall.getImsCall(), ImsPhoneCall.State.HOLDING,
                      DisconnectCause.NOT_DISCONNECTED);
              }
  
              // Check if the merge was requested by an existing conference call. In that
              // case, no further action is required.
              if (!call.isMergeRequestedByConf()) {
                  log("onCallMerged :: calling onMultipartyStateChanged()");
                  onMultipartyStateChanged(call, true);
              } else {
                  log("onCallMerged :: Merge requested by existing conference.");
                  // Reset the flag.
                  call.resetIsMergeRequestedByConf(false);
              }
              logState();
          }
  
          @Override
          public void onCallMergeFailed(ImsCall call, ImsReasonInfo reasonInfo) {
              if (DBG) log("onCallMergeFailed reasonInfo=" + reasonInfo);
  
              // TODO: the call to notifySuppServiceFailed throws up the "merge failed" dialog
              // We should move this into the InCallService so that it is handled appropriately
              // based on the user facing UI.
              mPhone.notifySuppServiceFailed(Phone.SuppService.CONFERENCE);
  
              call.resetIsMergeRequestedByConf(false);
  
              // Start plumbing this even through Telecom so other components can take
              // appropriate action.
              ImsPhoneConnection conn = findConnection(call);
              if (conn != null) {
                  conn.onConferenceMergeFailed();
                  conn.handleMergeComplete();
              }
          }
  
          private void updateConferenceParticipantsTiming(List<ConferenceParticipant> participants) {
              for (ConferenceParticipant participant : participants) {
                  // Every time participants are newly created from parcel, update their connect time.
                  CacheEntry cachedConnectTime = findConnectionTimeUsePhoneNumber(participant);
                  if (cachedConnectTime != null) {
                      participant.setConnectTime(cachedConnectTime.mConnectTime);
                      participant.setConnectElapsedTime(cachedConnectTime.mConnectElapsedTime);
                      participant.setCallDirection(cachedConnectTime.mCallDirection);
                  }
              }
          }
  
          /**
           * Called when the state of IMS conference participant(s) has changed.
           *
           * @param call the call object that carries out the IMS call.
           * @param participants the participant(s) and their new state information.
           */
          @Override
          public void onConferenceParticipantsStateChanged(ImsCall call,
                  List<ConferenceParticipant> participants) {
              if (DBG) log("onConferenceParticipantsStateChanged");
  
              ImsPhoneConnection conn = findConnection(call);
              if (conn != null) {
                  updateConferenceParticipantsTiming(participants);
                  conn.updateConferenceParticipants(participants);
              }
          }
  
          @Override
          public void onCallSessionTtyModeReceived(ImsCall call, int mode) {
              mPhone.onTtyModeReceived(mode);
          }
  
          @Override
          public void onCallHandover(ImsCall imsCall, int srcAccessTech, int targetAccessTech,
              ImsReasonInfo reasonInfo) {
              // Check with the DCTracker to see if data is enabled; there may be a case when
              // ImsPhoneCallTracker isn't being informed of the right data enabled state via its
              // registration, so we'll refresh now.
              boolean isDataEnabled = mPhone.getDefaultPhone().getDataEnabledSettings()
                      .isDataEnabled();
  
              if (DBG) {
                  log("onCallHandover ::  srcAccessTech=" + srcAccessTech + ", targetAccessTech="
                          + targetAccessTech + ", reasonInfo=" + reasonInfo + ", dataEnabled="
                          + mIsDataEnabled + "/" + isDataEnabled + ", dataMetered="
                          + mIsViLteDataMetered);
              }
              if (mIsDataEnabled != isDataEnabled) {
                  loge("onCallHandover: data enabled state doesn't match! (was=" + mIsDataEnabled
                          + ", actually=" + isDataEnabled);
                  mIsDataEnabled = isDataEnabled;
              }
  
              // Only consider it a valid handover to WIFI if the source radio tech is known.
              boolean isHandoverToWifi = srcAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN
                      && srcAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
                      && targetAccessTech == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN;
              // Only consider it a handover from WIFI if the source and target radio tech is known.
              boolean isHandoverFromWifi =
                      srcAccessTech == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
                              && targetAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN
                              && targetAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN;
  
              ImsPhoneConnection conn = findConnection(imsCall);
              if (conn != null) {
                  ImsPhoneCall imsPhoneCall = conn.getCall();
                  if (imsPhoneCall != null) {
                      // We might be playing ringback on the handover connection; we should stop
                      // playing it at this point (otherwise it could play indefinitely).
                      imsPhoneCall.maybeStopRingback();
                  }
                  if (conn.getDisconnectCause() == DisconnectCause.NOT_DISCONNECTED) {
                      if (isHandoverToWifi) {
                          removeMessages(EVENT_CHECK_FOR_WIFI_HANDOVER);
  
                          if (mNotifyHandoverVideoFromLTEToWifi && mHasAttemptedStartOfCallHandover) {
                              // This is a handover which happened mid-call (ie not the start of call
                              // handover from LTE to WIFI), so we'll notify the InCall UI.
                              conn.onConnectionEvent(
                                      TelephonyManager.EVENT_HANDOVER_VIDEO_FROM_LTE_TO_WIFI, null);
                          }
  
                          // We are on WIFI now so no need to get notified of network availability.
                          unregisterForConnectivityChanges();
                      } else if (isHandoverFromWifi && imsCall.isVideoCall()) {
                          // A video call just dropped from WIFI to LTE; we want to be informed if a
                          // new WIFI
                          // network comes into range.
                          registerForConnectivityChanges();
                      }
                  }
  
                  if (isHandoverToWifi && mIsViLteDataMetered) {
                      conn.setLocalVideoCapable(true);
                  }
  
                  if (isHandoverFromWifi && imsCall.isVideoCall()) {
                      if (mIsViLteDataMetered) {
                          conn.setLocalVideoCapable(mIsDataEnabled);
                      }
  
                      if (mNotifyHandoverVideoFromWifiToLTE && mIsDataEnabled) {
                          if (conn.getDisconnectCause() == DisconnectCause.NOT_DISCONNECTED) {
                              log("onCallHandover :: notifying of WIFI to LTE handover.");
                              conn.onConnectionEvent(
                                      TelephonyManager.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE, null);
                          } else {
                              // Call has already had a disconnect request issued by the user or is
                              // in the process of disconnecting; do not inform the UI of this as it
                              // is not relevant.
                              log("onCallHandover :: skip notify of WIFI to LTE handover for "
                                      + "disconnected call.");
                          }
                      }
  
                      if (!mIsDataEnabled && mIsViLteDataMetered) {
                          // Call was downgraded from WIFI to LTE and data is metered; downgrade the
                          // call now.
                          log("onCallHandover :: data is not enabled; attempt to downgrade.");
                          downgradeVideoCall(ImsReasonInfo.CODE_WIFI_LOST, conn);
                      }
                  }
              } else {
                  loge("onCallHandover :: connection null.");
              }
              // If there's a handover, then we're not in the "start of call" handover phase.
              if (!mHasAttemptedStartOfCallHandover) {
                  mHasAttemptedStartOfCallHandover = true;
              }
              mMetrics.writeOnImsCallHandoverEvent(mPhone.getPhoneId(),
                      TelephonyCallSession.Event.Type.IMS_CALL_HANDOVER, imsCall.getCallSession(),
                      srcAccessTech, targetAccessTech, reasonInfo);
          }
  
          @Override
          public void onCallHandoverFailed(ImsCall imsCall, int srcAccessTech, int targetAccessTech,
              ImsReasonInfo reasonInfo) {
              if (DBG) {
                  log("onCallHandoverFailed :: srcAccessTech=" + srcAccessTech +
                      ", targetAccessTech=" + targetAccessTech + ", reasonInfo=" + reasonInfo);
              }
              mMetrics.writeOnImsCallHandoverEvent(mPhone.getPhoneId(),
                      TelephonyCallSession.Event.Type.IMS_CALL_HANDOVER_FAILED,
                      imsCall.getCallSession(), srcAccessTech, targetAccessTech, reasonInfo);
  
              boolean isHandoverToWifi = srcAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN &&
                      targetAccessTech == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN;
              ImsPhoneConnection conn = findConnection(imsCall);
              if (conn != null && isHandoverToWifi) {
                  log("onCallHandoverFailed - handover to WIFI Failed");
  
                  // If we know we failed to handover, don't check for failure in the future.
                  removeMessages(EVENT_CHECK_FOR_WIFI_HANDOVER);
  
                  if (imsCall.isVideoCall()
                          && conn.getDisconnectCause() == DisconnectCause.NOT_DISCONNECTED) {
                      // Start listening for a WIFI network to come into range for potential handover.
                      registerForConnectivityChanges();
                  }
  
                  if (mNotifyVtHandoverToWifiFail) {
                      // Only notify others if carrier config indicates to do so.
                      conn.onHandoverToWifiFailed();
                  }
              }
              if (!mHasAttemptedStartOfCallHandover) {
                  mHasAttemptedStartOfCallHandover = true;
              }
          }
  
          @Override
          public void onRttModifyRequestReceived(ImsCall imsCall) {
              ImsPhoneConnection conn = findConnection(imsCall);
              if (conn != null) {
                  conn.onRttModifyRequestReceived();
              }
          }
  
          @Override
          public void onRttModifyResponseReceived(ImsCall imsCall, int status) {
              ImsPhoneConnection conn = findConnection(imsCall);
              if (conn != null) {
                  conn.onRttModifyResponseReceived(status);
              }
          }
  
          @Override
          public void onRttMessageReceived(ImsCall imsCall, String message) {
              ImsPhoneConnection conn = findConnection(imsCall);
              if (conn != null) {
                  conn.onRttMessageReceived(message);
              }
          }
  
          @Override
          public void onRttAudioIndicatorChanged(ImsCall imsCall, ImsStreamMediaProfile profile) {
            ImsPhoneConnection conn = findConnection(imsCall);
              if (conn != null) {
                  conn.onRttAudioIndicatorChanged(profile);
              }
          }
  
          /**
           * Handles a change to the multiparty state for an {@code ImsCall}.  Notifies the associated
           * {@link ImsPhoneConnection} of the change.
           *
           * @param imsCall The IMS call.
           * @param isMultiParty {@code true} if the call became multiparty, {@code false}
           *      otherwise.
           */
          @Override
          public void onMultipartyStateChanged(ImsCall imsCall, boolean isMultiParty) {
              if (DBG) log("onMultipartyStateChanged to " + (isMultiParty ? "Y" : "N"));
  
              ImsPhoneConnection conn = findConnection(imsCall);
              if (conn != null) {
                  conn.updateMultipartyState(isMultiParty);
              }
          }
  
          /**
           * Handles a change to the call quality for an {@code ImsCall}.
           * Notifies apps through the System API {@link PhoneStateListener#onCallAttributesChanged}.
           */
          @Override
          public void onCallQualityChanged(ImsCall imsCall, CallQuality callQuality) {
              // convert ServiceState.radioTech to TelephonyManager.NetworkType constant
              mPhone.onCallQualityChanged(callQuality,
                      ServiceState.rilRadioTechnologyToNetworkType(imsCall.getRadioTechnology()));
              String callId = imsCall.getSession().getCallId();
              CallQualityMetrics cqm = mCallQualityMetrics.get(callId);
              if (cqm == null) {
                  cqm = new CallQualityMetrics(mPhone);
              }
              cqm.saveCallQuality(callQuality);
              mCallQualityMetrics.put(callId, cqm);
          }
      };
  
      /**
       * Listen to the IMS call state change
       */
      private ImsCall.Listener mImsUssdListener = new ImsCall.Listener() {
          @Override
          public void onCallStarted(ImsCall imsCall) {
              if (DBG) log("mImsUssdListener onCallStarted");
  
              if (imsCall == mUssdSession) {
                  if (mPendingUssd != null) {
                      AsyncResult.forMessage(mPendingUssd);
                      mPendingUssd.sendToTarget();
                      mPendingUssd = null;
                  }
              }
          }
  
          @Override
          public void onCallStartFailed(ImsCall imsCall, ImsReasonInfo reasonInfo) {
              if (DBG) log("mImsUssdListener onCallStartFailed reasonCode=" + reasonInfo.getCode());
  
              onCallTerminated(imsCall, reasonInfo);
          }
  
          @Override
          public void onCallTerminated(ImsCall imsCall, ImsReasonInfo reasonInfo) {
              if (DBG) log("mImsUssdListener onCallTerminated reasonCode=" + reasonInfo.getCode());
              removeMessages(EVENT_CHECK_FOR_WIFI_HANDOVER);
              mHasAttemptedStartOfCallHandover = false;
              unregisterForConnectivityChanges();
  
              if (imsCall == mUssdSession) {
                  mUssdSession = null;
                  if (mPendingUssd != null) {
                      CommandException ex =
                              new CommandException(CommandException.Error.GENERIC_FAILURE);
                      AsyncResult.forMessage(mPendingUssd, null, ex);
                      mPendingUssd.sendToTarget();
                      mPendingUssd = null;
                  }
              }
              imsCall.close();
          }
  
          @Override
          public void onCallUssdMessageReceived(ImsCall call,
                  int mode, String ussdMessage) {
              if (DBG) log("mImsUssdListener onCallUssdMessageReceived mode=" + mode);
  
              int ussdMode = -1;
  
              switch(mode) {
                  case ImsCall.USSD_MODE_REQUEST:
                      ussdMode = CommandsInterface.USSD_MODE_REQUEST;
                      break;
  
                  case ImsCall.USSD_MODE_NOTIFY:
                      ussdMode = CommandsInterface.USSD_MODE_NOTIFY;
                      break;
              }
  
              mPhone.onIncomingUSSD(ussdMode, ussdMessage);
          }
      };
  
      private final ImsMmTelManager.RegistrationCallback mImsRegistrationCallback =
              new ImsMmTelManager.RegistrationCallback() {
  
                  @Override
                  public void onRegistered(
                          @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) {
                      if (DBG) log("onImsConnected imsRadioTech=" + imsRadioTech);
                      mPhone.setServiceState(ServiceState.STATE_IN_SERVICE);
                      mPhone.setImsRegistered(true);
                      mMetrics.writeOnImsConnectionState(mPhone.getPhoneId(),
                              ImsConnectionState.State.CONNECTED, null);
                  }
  
                  @Override
                  public void onRegistering(
                          @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) {
                      if (DBG) log("onImsProgressing imsRadioTech=" + imsRadioTech);
                      mPhone.setServiceState(ServiceState.STATE_OUT_OF_SERVICE);
                      mPhone.setImsRegistered(false);
                      mMetrics.writeOnImsConnectionState(mPhone.getPhoneId(),
                              ImsConnectionState.State.PROGRESSING, null);
                  }
  
                  @Override
                  public void onUnregistered(ImsReasonInfo imsReasonInfo) {
                      if (DBG) log("onImsDisconnected imsReasonInfo=" + imsReasonInfo);
                      mPhone.setServiceState(ServiceState.STATE_OUT_OF_SERVICE);
                      mPhone.setImsRegistered(false);
                      mPhone.processDisconnectReason(imsReasonInfo);
                      mMetrics.writeOnImsConnectionState(mPhone.getPhoneId(),
                              ImsConnectionState.State.DISCONNECTED, imsReasonInfo);
                  }
  
                  @Override
                  public void onSubscriberAssociatedUriChanged(Uri[] uris) {
                      if (DBG) log("registrationAssociatedUriChanged");
                      mPhone.setCurrentSubscriberUris(uris);
                  }
              };
  
      private final ImsMmTelManager.CapabilityCallback mImsCapabilityCallback =
              new ImsMmTelManager.CapabilityCallback() {
                  @Override
                  public void onCapabilitiesStatusChanged(
                          MmTelFeature.MmTelCapabilities capabilities) {
                      if (DBG) log("onCapabilitiesStatusChanged: " + capabilities);
                      SomeArgs args = SomeArgs.obtain();
                      args.arg1 = capabilities;
                      // Remove any pending updates; they're already stale, so no need to process
                      // them.
                      removeMessages(EVENT_ON_FEATURE_CAPABILITY_CHANGED);
                      obtainMessage(EVENT_ON_FEATURE_CAPABILITY_CHANGED, args).sendToTarget();
                  }
              };
  
      private ImsConfigListener.Stub mImsConfigListener = new ImsConfigListener.Stub() {
          @Override
          public void onGetFeatureResponse(int feature, int network, int value, int status) {}
  
          @Override
          public void onSetFeatureResponse(int feature, int network, int value, int status) {
              mMetrics.writeImsSetFeatureValue(mPhone.getPhoneId(), feature, network, value);
          }
  
          @Override
          public void onGetVideoQuality(int status, int quality) {}
  
          @Override
          public void onSetVideoQuality(int status) {}
  
      };
  
      private final ProvisioningManager.Callback mConfigCallback =
              new ProvisioningManager.Callback() {
          @Override
          public void onProvisioningIntChanged(int item, int value) {
              sendConfigChangedIntent(item, Integer.toString(value));
          }
  
          @Override
          public void onProvisioningStringChanged(int item, String value) {
              sendConfigChangedIntent(item, value);
          }
  
          // send IMS_CONFIG_CHANGED intent for older services that do not implement the new callback
          // interface.
          private void sendConfigChangedIntent(int item, String value) {
              log("sendConfigChangedIntent - [" + item + ", " + value + "]");
              Intent configChangedIntent = new Intent(ImsConfig.ACTION_IMS_CONFIG_CHANGED);
              configChangedIntent.putExtra(ImsConfig.EXTRA_CHANGED_ITEM, item);
              configChangedIntent.putExtra(ImsConfig.EXTRA_NEW_VALUE, value);
              if (mPhone != null && mPhone.getContext() != null) {
                  mPhone.getContext().sendBroadcast(configChangedIntent);
              }
          }
      };
  
      public void sendCallStartFailedDisconnect(ImsCall imsCall, ImsReasonInfo reasonInfo) {
          mPendingMO = null;
          ImsPhoneConnection conn = findConnection(imsCall);
          Call.State callState;
          if (conn != null) {
              callState = conn.getState();
          } else {
              // Need to fall back in case connection is null; it shouldn't be, but a sane
              // fallback is to assume we're dialing.  This state is only used to
              // determine which disconnect string to show in the case of a low battery
              // disconnect.
              callState = Call.State.DIALING;
          }
          int cause = getDisconnectCauseFromReasonInfo(reasonInfo, callState);
  
          processCallStateChange(imsCall, ImsPhoneCall.State.DISCONNECTED, cause);
          mPhone.notifyImsReason(reasonInfo);
      }
  
      @UnsupportedAppUsage
      public ImsUtInterface getUtInterface() throws ImsException {
          if (mImsManager == null) {
              throw getImsManagerIsNullException();
          }
  
          ImsUtInterface ut = mImsManager.getSupplementaryServiceConfiguration();
          return ut;
      }
  
      private void transferHandoverConnections(ImsPhoneCall call) {
          if (call.mConnections != null) {
              for (Connection c : call.mConnections) {
                  c.mPreHandoverState = call.mState;
                  log ("Connection state before handover is " + c.getStateBeforeHandover());
              }
          }
          if (mHandoverCall.mConnections == null ) {
              mHandoverCall.mConnections = call.mConnections;
          } else { // Multi-call SRVCC
              mHandoverCall.mConnections.addAll(call.mConnections);
          }
          if (mHandoverCall.mConnections != null) {
              if (call.getImsCall() != null) {
                  call.getImsCall().close();
              }
              for (Connection c : mHandoverCall.mConnections) {
                  ((ImsPhoneConnection)c).changeParent(mHandoverCall);
                  ((ImsPhoneConnection)c).releaseWakeLock();
              }
          }
          if (call.getState().isAlive()) {
              log ("Call is alive and state is " + call.mState);
              mHandoverCall.mState = call.mState;
          }
          call.mConnections.clear();
          call.mState = ImsPhoneCall.State.IDLE;
          if (mPendingMO != null) {
              // If the call is handed over before moving to alerting (i.e. e911 CSFB redial), clear
              // pending MO here.
              logi("pending MO on handover, clearing...");
              mPendingMO = null;
          }
      }
  
      /* package */
      void notifySrvccState(Call.SrvccState state) {
          if (DBG) log("notifySrvccState state=" + state);
  
          mSrvccState = state;
  
          if (mSrvccState == Call.SrvccState.COMPLETED) {
              resetState();
              transferHandoverConnections(mForegroundCall);
              transferHandoverConnections(mBackgroundCall);
              transferHandoverConnections(mRingingCall);
          }
      }
  
      private void resetState() {
          mIsInEmergencyCall = false;
      }
  
      //****** Overridden from Handler
  
      @Override
      public void
      handleMessage (Message msg) {
          AsyncResult ar;
          if (DBG) log("handleMessage what=" + msg.what);
  
          switch (msg.what) {
              case EVENT_HANGUP_PENDINGMO:
                  if (mPendingMO != null) {
                      mPendingMO.onDisconnect();
                      removeConnection(mPendingMO);
                      mPendingMO = null;
                  }
                  mPendingIntentExtras = null;
                  updatePhoneState();
                  mPhone.notifyPreciseCallStateChanged();
                  break;
              case EVENT_RESUME_NOW_FOREGROUND_CALL:
                  try {
                      resumeForegroundCall();
                  } catch (ImsException e) {
                      if (Phone.DEBUG_PHONE) {
                          loge("handleMessage EVENT_RESUME_NOW_FOREGROUND_CALL exception=" + e);
                      }
                  }
                  break;
              case EVENT_ANSWER_WAITING_CALL:
                  try {
                      answerWaitingCall();
                  } catch (ImsException e) {
                      if (Phone.DEBUG_PHONE) {
                          loge("handleMessage EVENT_ANSWER_WAITING_CALL exception=" + e);
                      }
                  }
                  break;
              case EVENT_DIAL_PENDINGMO:
                  dialInternal(mPendingMO, mClirMode, mPendingCallVideoState, mPendingIntentExtras);
                  mPendingIntentExtras = null;
                  break;
  
              case EVENT_EXIT_ECBM_BEFORE_PENDINGMO:
                  if (mPendingMO != null) {
                      //Send ECBM exit request
                      try {
                          getEcbmInterface().exitEmergencyCallbackMode();
                          mPhone.setOnEcbModeExitResponse(this, EVENT_EXIT_ECM_RESPONSE_CDMA, null);
                          pendingCallClirMode = mClirMode;
                          pendingCallInEcm = true;
                      } catch (ImsException e) {
                          e.printStackTrace();
                          mPendingMO.setDisconnectCause(DisconnectCause.ERROR_UNSPECIFIED);
                          sendEmptyMessageDelayed(EVENT_HANGUP_PENDINGMO, TIMEOUT_HANGUP_PENDINGMO);
                      }
                  }
                  break;
  
              case EVENT_EXIT_ECM_RESPONSE_CDMA:
                  // no matter the result, we still do the same here
                  if (pendingCallInEcm) {
                      dialInternal(mPendingMO, pendingCallClirMode,
                              mPendingCallVideoState, mPendingIntentExtras);
                      mPendingIntentExtras = null;
                      pendingCallInEcm = false;
                  }
                  mPhone.unsetOnEcbModeExitResponse(this);
                  break;
              case EVENT_VT_DATA_USAGE_UPDATE:
                  ar = (AsyncResult) msg.obj;
                  ImsCall call = (ImsCall) ar.userObj;
                  Long usage = (long) ar.result;
                  log("VT data usage update. usage = " + usage + ", imsCall = " + call);
                  if (usage > 0) {
                      updateVtDataUsage(call, usage);
                  }
                  break;
              case EVENT_DATA_ENABLED_CHANGED:
                  ar = (AsyncResult) msg.obj;
                  if (ar.result instanceof Pair) {
                      Pair<Boolean, Integer> p = (Pair<Boolean, Integer>) ar.result;
                      onDataEnabledChanged(p.first, p.second);
                  }
                  break;
              case EVENT_CHECK_FOR_WIFI_HANDOVER:
                  if (msg.obj instanceof ImsCall) {
                      ImsCall imsCall = (ImsCall) msg.obj;
                      if (imsCall != mForegroundCall.getImsCall()) {
                          Rlog.i(LOG_TAG, "handoverCheck: no longer FG; check skipped.");
                          unregisterForConnectivityChanges();
                          // Handover check and its not the foreground call any more.
                          return;
                      }
                      if (!mHasAttemptedStartOfCallHandover) {
                          mHasAttemptedStartOfCallHandover = true;
                      }
                      if (!imsCall.isWifiCall()) {
                          // Call did not handover to wifi, notify of handover failure.
                          ImsPhoneConnection conn = findConnection(imsCall);
                          if (conn != null) {
                              Rlog.i(LOG_TAG, "handoverCheck: handover failed.");
                              conn.onHandoverToWifiFailed();
                          }
  
                          if (imsCall.isVideoCall()
                                  && conn.getDisconnectCause() == DisconnectCause.NOT_DISCONNECTED) {
                              registerForConnectivityChanges();
                          }
                      }
                  }
                  break;
              case EVENT_ON_FEATURE_CAPABILITY_CHANGED: {
                  SomeArgs args = (SomeArgs) msg.obj;
                  try {
                      ImsFeature.Capabilities capabilities = (ImsFeature.Capabilities) args.arg1;
                      handleFeatureCapabilityChanged(capabilities);
                  } finally {
                      args.recycle();
                  }
                  break;
              }
              case EVENT_SUPP_SERVICE_INDICATION: {
                  ar = (AsyncResult) msg.obj;
                  ImsPhoneMmiCode mmiCode = new ImsPhoneMmiCode(mPhone);
                  try {
                      mmiCode.setIsSsInfo(true);
                      mmiCode.processImsSsData(ar);
                  } catch (ImsException e) {
                      Rlog.e(LOG_TAG, "Exception in parsing SS Data: " + e);
                  }
                  break;
              }
              case EVENT_REDIAL_WIFI_E911_CALL: {
                  Pair<ImsCall, ImsReasonInfo> callInfo =
                          (Pair<ImsCall, ImsReasonInfo>) ((AsyncResult) msg.obj).userObj;
                  removeMessages(EVENT_REDIAL_WIFI_E911_TIMEOUT);
                  mPhone.getDefaultPhone().getServiceStateTracker()
                          .unregisterForNetworkAttached(this);
                  ImsPhoneConnection oldConnection = findConnection(callInfo.first);
                  if (oldConnection == null) {
                      sendCallStartFailedDisconnect(callInfo.first, callInfo.second);
                      break;
                  }
                  mForegroundCall.detach(oldConnection);
                  removeConnection(oldConnection);
                  try {
                      Connection newConnection =
                              mPhone.getDefaultPhone().dial(mLastDialString, mLastDialArgs);
                      oldConnection.onOriginalConnectionReplaced(newConnection);
                  } catch (CallStateException e) {
                      sendCallStartFailedDisconnect(callInfo.first, callInfo.second);
                  }
                  break;
              }
              case EVENT_REDIAL_WIFI_E911_TIMEOUT: {
                  Pair<ImsCall, ImsReasonInfo> callInfo = (Pair<ImsCall, ImsReasonInfo>) msg.obj;
                  mPhone.getDefaultPhone().getServiceStateTracker()
                          .unregisterForNetworkAttached(this);
                  removeMessages(EVENT_REDIAL_WIFI_E911_CALL);
                  sendCallStartFailedDisconnect(callInfo.first, callInfo.second);
                  break;
              }
  
              case EVENT_REDIAL_WITHOUT_RTT: {
                  Pair<ImsCall, ImsReasonInfo> callInfo = (Pair<ImsCall, ImsReasonInfo>) msg.obj;
                  removeMessages(EVENT_REDIAL_WITHOUT_RTT);
                  ImsPhoneConnection oldConnection = findConnection(callInfo.first);
                  if (oldConnection == null) {
                      sendCallStartFailedDisconnect(callInfo.first, callInfo.second);
                      break;
                  }
                  mForegroundCall.detach(oldConnection);
                  removeConnection(oldConnection);
                  try {
                      mPendingMO = null;
                      ImsDialArgs newDialArgs = ImsDialArgs.Builder.from(mLastDialArgs)
                              .setRttTextStream(null)
                              .build();
                      Connection newConnection =
                              mPhone.getDefaultPhone().dial(mLastDialString, newDialArgs);
                      oldConnection.onOriginalConnectionReplaced(newConnection);
                  } catch (CallStateException e) {
                      sendCallStartFailedDisconnect(callInfo.first, callInfo.second);
                  }
                  break;
              }
          }
      }
  
      /**
       * Update video call data usage
       *
       * @param call The IMS call
       * @param dataUsage The aggregated data usage for the call
       */
      private void updateVtDataUsage(ImsCall call, long dataUsage) {
          long oldUsage = 0L;
          if (mVtDataUsageMap.containsKey(call.uniqueId)) {
              oldUsage = mVtDataUsageMap.get(call.uniqueId);
          }
  
          long delta = dataUsage - oldUsage;
          mVtDataUsageMap.put(call.uniqueId, dataUsage);
  
          log("updateVtDataUsage: call=" + call + ", delta=" + delta);
  
          long currentTime = SystemClock.elapsedRealtime();
          int isRoaming = mPhone.getServiceState().getDataRoaming() ? 1 : 0;
  
          // Create the snapshot of total video call data usage.
          NetworkStats vtDataUsageSnapshot = new NetworkStats(currentTime, 1);
          vtDataUsageSnapshot.combineAllValues(mVtDataUsageSnapshot);
          // Since the modem only reports the total vt data usage rather than rx/tx separately,
          // the only thing we can do here is splitting the usage into half rx and half tx.
          // Uid -1 indicates this is for the overall device data usage.
          vtDataUsageSnapshot.combineValues(new NetworkStats.Entry(
                  NetworkStatsService.VT_INTERFACE, -1, NetworkStats.SET_FOREGROUND,
                  NetworkStats.TAG_NONE, NetworkStats.METERED_YES, isRoaming,
                  NetworkStats.DEFAULT_NETWORK_YES, delta / 2, 0, delta / 2, 0, 0));
          mVtDataUsageSnapshot = vtDataUsageSnapshot;
  
          // Create the snapshot of video call data usage per dialer. combineValues will create
          // a separate entry if uid is different from the previous snapshot.
          NetworkStats vtDataUsageUidSnapshot = new NetworkStats(currentTime, 1);
          vtDataUsageUidSnapshot.combineAllValues(mVtDataUsageUidSnapshot);
  
          // The dialer uid might not be initialized correctly during boot up due to telecom service
          // not ready or its default dialer cache not ready. So we double check again here to see if
          // default dialer uid is really not available.
          if (mDefaultDialerUid.get() == NetworkStats.UID_ALL) {
              final TelecomManager telecomManager =
                      (TelecomManager) mPhone.getContext().getSystemService(Context.TELECOM_SERVICE);
              mDefaultDialerUid.set(
                      getPackageUid(mPhone.getContext(), telecomManager.getDefaultDialerPackage()));
          }
  
          // Since the modem only reports the total vt data usage rather than rx/tx separately,
          // the only thing we can do here is splitting the usage into half rx and half tx.
          vtDataUsageUidSnapshot.combineValues(new NetworkStats.Entry(
                  NetworkStatsService.VT_INTERFACE, mDefaultDialerUid.get(),
                  NetworkStats.SET_FOREGROUND, NetworkStats.TAG_NONE, NetworkStats.METERED_YES,
                  isRoaming, NetworkStats.DEFAULT_NETWORK_YES, delta / 2, 0, delta / 2, 0, 0));
          mVtDataUsageUidSnapshot = vtDataUsageUidSnapshot;
      }
  
      @UnsupportedAppUsage
      @Override
      protected void log(String msg) {
          Rlog.d(LOG_TAG, "[" + mPhone.getPhoneId() + "] " + msg);
      }
  
      @UnsupportedAppUsage
      protected void loge(String msg) {
          Rlog.e(LOG_TAG, "[" + mPhone.getPhoneId() + "] " + msg);
      }
  
      void logi(String msg) {
          Rlog.i(LOG_TAG, "[" + mPhone.getPhoneId() + "] " + msg);
      }
  
      void logHoldSwapState(String loc) {
          String holdSwapState = "???";
          switch (mHoldSwitchingState) {
              case INACTIVE:
                  holdSwapState = "INACTIVE";
                  break;
              case PENDING_SINGLE_CALL_HOLD:
                  holdSwapState = "PENDING_SINGLE_CALL_HOLD";
                  break;
              case PENDING_SINGLE_CALL_UNHOLD:
                  holdSwapState = "PENDING_SINGLE_CALL_UNHOLD";
                  break;
              case SWAPPING_ACTIVE_AND_HELD:
                  holdSwapState = "SWAPPING_ACTIVE_AND_HELD";
                  break;
              case HOLDING_TO_ANSWER_INCOMING:
                  holdSwapState = "HOLDING_TO_ANSWER_INCOMING";
                  break;
              case PENDING_RESUME_FOREGROUND_AFTER_FAILURE:
                  holdSwapState = "PENDING_RESUME_FOREGROUND_AFTER_FAILURE";
                  break;
              case HOLDING_TO_DIAL_OUTGOING:
                  holdSwapState = "HOLDING_TO_DIAL_OUTGOING";
                  break;
          }
          logi("holdSwapState set to " + holdSwapState + " at " + loc);
      }
  
      /**
       * Logs the current state of the ImsPhoneCallTracker.  Useful for debugging issues with
       * call tracking.
       */
      /* package */
      void logState() {
          if (!VERBOSE_STATE_LOGGING) {
              return;
          }
  
          StringBuilder sb = new StringBuilder();
          sb.append("Current IMS PhoneCall State:\n");
          sb.append(" Foreground: ");
          sb.append(mForegroundCall);
          sb.append("\n");
          sb.append(" Background: ");
          sb.append(mBackgroundCall);
          sb.append("\n");
          sb.append(" Ringing: ");
          sb.append(mRingingCall);
          sb.append("\n");
          sb.append(" Handover: ");
          sb.append(mHandoverCall);
          sb.append("\n");
          Rlog.v(LOG_TAG, sb.toString());
      }
  
      @Override
      public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
          pw.println("ImsPhoneCallTracker extends:");
          super.dump(fd, pw, args);
          pw.println(" mVoiceCallEndedRegistrants=" + mVoiceCallEndedRegistrants);
          pw.println(" mVoiceCallStartedRegistrants=" + mVoiceCallStartedRegistrants);
          pw.println(" mRingingCall=" + mRingingCall);
          pw.println(" mForegroundCall=" + mForegroundCall);
          pw.println(" mBackgroundCall=" + mBackgroundCall);
          pw.println(" mHandoverCall=" + mHandoverCall);
          pw.println(" mPendingMO=" + mPendingMO);
          //pw.println(" mHangupPendingMO=" + mHangupPendingMO);
          pw.println(" mPhone=" + mPhone);
          pw.println(" mDesiredMute=" + mDesiredMute);
          pw.println(" mState=" + mState);
          pw.println(" mMmTelCapabilities=" + mMmTelCapabilities);
          pw.println(" mDefaultDialerUid=" + mDefaultDialerUid.get());
          pw.println(" mVtDataUsageSnapshot=" + mVtDataUsageSnapshot);
          pw.println(" mVtDataUsageUidSnapshot=" + mVtDataUsageUidSnapshot);
          pw.println(" mCallQualityMetrics=" + mCallQualityMetrics);
          pw.println(" mCallQualityMetricsHistory=" + mCallQualityMetricsHistory);
  
          pw.flush();
          pw.println("++++++++++++++++++++++++++++++++");
  
          try {
              if (mImsManager != null) {
                  mImsManager.dump(fd, pw, args);
              }
          } catch (Exception e) {
              e.printStackTrace();
          }
  
          if (mConnections != null && mConnections.size() > 0) {
              pw.println("mConnections:");
              for (int i = 0; i < mConnections.size(); i++) {
                  pw.println("  [" + i + "]: " + mConnections.get(i));
              }
          }
      }
  
      @Override
      protected void handlePollCalls(AsyncResult ar) {
      }
  
      /* package */
      ImsEcbm getEcbmInterface() throws ImsException {
          if (mImsManager == null) {
              throw getImsManagerIsNullException();
          }
  
          ImsEcbm ecbm = mImsManager.getEcbmInterface();
          return ecbm;
      }
  
      /* package */
      ImsMultiEndpoint getMultiEndpointInterface() throws ImsException {
          if (mImsManager == null) {
              throw getImsManagerIsNullException();
          }
  
          try {
              return mImsManager.getMultiEndpointInterface();
          } catch (ImsException e) {
              if (e.getCode() == ImsReasonInfo.CODE_MULTIENDPOINT_NOT_SUPPORTED) {
                  return null;
              } else {
                  throw e;
              }
  
          }
      }
  
      public boolean isInEmergencyCall() {
          return mIsInEmergencyCall;
      }
  
      /**
       * @return true if the IMS capability for the specified registration technology is currently
       * available.
       */
      public boolean isImsCapabilityAvailable(int capability, int regTech) {
          return (getImsRegistrationTech() == regTech) && mMmTelCapabilities.isCapable(capability);
      }
  
      public boolean isVolteEnabled() {
          boolean isRadioTechLte = getImsRegistrationTech()
                  == ImsRegistrationImplBase.REGISTRATION_TECH_LTE;
          return isRadioTechLte && mMmTelCapabilities.isCapable(
                  MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
      }
  
      public boolean isVowifiEnabled() {
          boolean isRadioTechIwlan = getImsRegistrationTech()
                  == ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
          return isRadioTechIwlan && mMmTelCapabilities.isCapable(
                  MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
      }
  
      public boolean isVideoCallEnabled() {
          return mMmTelCapabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
      }
  
      @Override
      public PhoneConstants.State getState() {
          return mState;
      }
  
      public int getImsRegistrationTech() {
          if (mImsManager != null) {
              return mImsManager.getRegistrationTech();
          }
          return ImsRegistrationImplBase.REGISTRATION_TECH_NONE;
      }
  
      private void retryGetImsService() {
          // The binder connection is already up. Do not try to get it again.
          if (mImsManager.isServiceAvailable()) {
              return;
          }
  
          mImsManagerConnector.connect();
      }
  
      private void setVideoCallProvider(ImsPhoneConnection conn, ImsCall imsCall)
              throws RemoteException {
          IImsVideoCallProvider imsVideoCallProvider =
                  imsCall.getCallSession().getVideoCallProvider();
          if (imsVideoCallProvider != null) {
              // TODO: Remove this when we can better formalize the format of session modify requests.
              boolean useVideoPauseWorkaround = mPhone.getContext().getResources().getBoolean(
                      com.android.internal.R.bool.config_useVideoPauseWorkaround);
  
              ImsVideoCallProviderWrapper imsVideoCallProviderWrapper =
                      new ImsVideoCallProviderWrapper(imsVideoCallProvider);
              if (useVideoPauseWorkaround) {
                  imsVideoCallProviderWrapper.setUseVideoPauseWorkaround(useVideoPauseWorkaround);
              }
              conn.setVideoProvider(imsVideoCallProviderWrapper);
              imsVideoCallProviderWrapper.registerForDataUsageUpdate
                      (this, EVENT_VT_DATA_USAGE_UPDATE, imsCall);
              imsVideoCallProviderWrapper.addImsVideoProviderCallback(conn);
          }
      }
  
      public boolean isUtEnabled() {
          return mMmTelCapabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT);
      }
  
      /**
       * Given a call subject, removes any characters considered by the current carrier to be
       * invalid, as well as escaping (using \) any characters which the carrier requires to be
       * escaped.
       *
       * @param callSubject The call subject.
       * @return The call subject with invalid characters removed and escaping applied as required.
       */
      private String cleanseInstantLetteringMessage(String callSubject) {
          if (TextUtils.isEmpty(callSubject)) {
              return callSubject;
          }
  
          // Get the carrier config for the current sub.
          CarrierConfigManager configMgr = (CarrierConfigManager)
                  mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
          // Bail if we can't find the carrier config service.
          if (configMgr == null) {
              return callSubject;
          }
  
          PersistableBundle carrierConfig = configMgr.getConfigForSubId(mPhone.getSubId());
          // Bail if no carrier config found.
          if (carrierConfig == null) {
              return callSubject;
          }
  
          // Try to replace invalid characters
          String invalidCharacters = carrierConfig.getString(
                  CarrierConfigManager.KEY_CARRIER_INSTANT_LETTERING_INVALID_CHARS_STRING);
          if (!TextUtils.isEmpty(invalidCharacters)) {
              callSubject = callSubject.replaceAll(invalidCharacters, "");
          }
  
          // Try to escape characters which need to be escaped.
          String escapedCharacters = carrierConfig.getString(
                  CarrierConfigManager.KEY_CARRIER_INSTANT_LETTERING_ESCAPED_CHARS_STRING);
          if (!TextUtils.isEmpty(escapedCharacters)) {
              callSubject = escapeChars(escapedCharacters, callSubject);
          }
          return callSubject;
      }
  
      /**
       * Given a source string, return a string where a set of characters are escaped using the
       * backslash character.
       *
       * @param toEscape The characters to escape with a backslash.
       * @param source The source string.
       * @return The source string with characters escaped.
       */
      private String escapeChars(String toEscape, String source) {
          StringBuilder escaped = new StringBuilder();
          for (char c : source.toCharArray()) {
              if (toEscape.contains(Character.toString(c))) {
                  escaped.append("\\");
              }
              escaped.append(c);
          }
  
          return escaped.toString();
      }
  
      /**
       * Initiates a pull of an external call.
       *
       * Initiates a pull by making a dial request with the {@link ImsCallProfile#EXTRA_IS_CALL_PULL}
       * extra specified.  We call {@link ImsPhone#notifyUnknownConnection(Connection)} which notifies
       * Telecom of the new dialed connection.  The
       * {@code PstnIncomingCallNotifier#maybeSwapWithUnknownConnection} logic ensures that the new
       * {@link ImsPhoneConnection} resulting from the dial gets swapped with the
       * {@link ImsExternalConnection}, which effectively makes the external call become a regular
       * call.  Magic!
       *
       * @param number The phone number of the call to be pulled.
       * @param videoState The desired video state of the pulled call.
       * @param dialogId The {@link ImsExternalConnection#getCallId()} dialog id associated with the
       *                 call which is being pulled.
       */
      @Override
      public void pullExternalCall(String number, int videoState, int dialogId) {
          Bundle extras = new Bundle();
          extras.putBoolean(ImsCallProfile.EXTRA_IS_CALL_PULL, true);
          extras.putInt(ImsExternalCallTracker.EXTRA_IMS_EXTERNAL_CALL_ID, dialogId);
          try {
              Connection connection = dial(number, videoState, extras);
              mPhone.notifyUnknownConnection(connection);
          } catch (CallStateException e) {
              loge("pullExternalCall failed - " + e);
          }
      }
  
      private ImsException getImsManagerIsNullException() {
          return new ImsException("no ims manager", ImsReasonInfo.CODE_LOCAL_ILLEGAL_STATE);
      }
  
      /**
       * Determines if answering an incoming call will cause the active call to be disconnected.
       * <p>
       * This will be the case if
       * {@link CarrierConfigManager#KEY_DROP_VIDEO_CALL_WHEN_ANSWERING_AUDIO_CALL_BOOL} is
       * {@code true} for the carrier, the active call is a video call over WIFI, and the incoming
       * call is an audio call.
       *
       * @param activeCall The active call.
       * @param incomingCall The incoming call.
       * @return {@code true} if answering the incoming call will cause the active call to be
       *      disconnected, {@code false} otherwise.
       */
      private boolean shouldDisconnectActiveCallOnAnswer(ImsCall activeCall,
              ImsCall incomingCall) {
  
          if (activeCall == null || incomingCall == null) {
              return false;
          }
  
          if (!mDropVideoCallWhenAnsweringAudioCall) {
              return false;
          }
  
          boolean isActiveCallVideo = activeCall.isVideoCall() ||
                  (mTreatDowngradedVideoCallsAsVideoCalls && activeCall.wasVideoCall());
          boolean isActiveCallOnWifi = activeCall.isWifiCall();
          boolean isVoWifiEnabled = mImsManager.isWfcEnabledByPlatform()
                  && mImsManager.isWfcEnabledByUser();
          boolean isIncomingCallAudio = !incomingCall.isVideoCall();
          log("shouldDisconnectActiveCallOnAnswer : isActiveCallVideo=" + isActiveCallVideo +
                  " isActiveCallOnWifi=" + isActiveCallOnWifi + " isIncomingCallAudio=" +
                  isIncomingCallAudio + " isVowifiEnabled=" + isVoWifiEnabled);
  
          return isActiveCallVideo && isActiveCallOnWifi && isIncomingCallAudio && !isVoWifiEnabled;
      }
  
      /**
       * Get aggregated video call data usage since boot.
       *
       * @param perUidStats True if requesting data usage per uid, otherwise overall usage.
       * @return Snapshot of video call data usage
       */
      public NetworkStats getVtDataUsage(boolean perUidStats) {
  
          // If there is an ongoing VT call, request the latest VT usage from the modem. The latest
          // usage will return asynchronously so it won't be counted in this round, but it will be
          // eventually counted when next getVtDataUsage is called.
          if (mState != PhoneConstants.State.IDLE) {
              for (ImsPhoneConnection conn : mConnections) {
                  android.telecom.Connection.VideoProvider videoProvider = conn.getVideoProvider();
                  if (videoProvider != null) {
                      videoProvider.onRequestConnectionDataUsage();
                  }
              }
          }
  
          return perUidStats ? mVtDataUsageUidSnapshot : mVtDataUsageSnapshot;
      }
  
      public void registerPhoneStateListener(PhoneStateListener listener) {
          mPhoneStateListeners.add(listener);
      }
  
      public void unregisterPhoneStateListener(PhoneStateListener listener) {
          mPhoneStateListeners.remove(listener);
      }
  
      /**
       * Notifies local telephony listeners of changes to the IMS phone state.
       *
       * @param oldState The old state.
       * @param newState The new state.
       */
      private void notifyPhoneStateChanged(PhoneConstants.State oldState,
              PhoneConstants.State newState) {
  
          for (PhoneStateListener listener : mPhoneStateListeners) {
              listener.onPhoneStateChanged(oldState, newState);
          }
      }
  
      /** Modify video call to a new video state.
       *
       * @param imsCall IMS call to be modified
       * @param newVideoState New video state. (Refer to VideoProfile)
       */
      private void modifyVideoCall(ImsCall imsCall, int newVideoState) {
          ImsPhoneConnection conn = findConnection(imsCall);
          if (conn != null) {
              int oldVideoState = conn.getVideoState();
              if (conn.getVideoProvider() != null) {
                  conn.getVideoProvider().onSendSessionModifyRequest(
                          new VideoProfile(oldVideoState), new VideoProfile(newVideoState));
              }
          }
      }
  
      public boolean isViLteDataMetered() {
          return mIsViLteDataMetered;
      }
  
      /**
       * Handler of data enabled changed event
       * @param enabled True if data is enabled, otherwise disabled.
       * @param reason Reason for data enabled/disabled. See {@link DataEnabledChangedReason}.
       */
      private void onDataEnabledChanged(boolean enabled, @DataEnabledChangedReason int reason) {
  
          log("onDataEnabledChanged: enabled=" + enabled + ", reason=" + reason);
  
          mIsDataEnabled = enabled;
  
          if (!mIsViLteDataMetered) {
              log("Ignore data " + ((enabled) ? "enabled" : "disabled") + " - carrier policy "
                      + "indicates that data is not metered for ViLTE calls.");
              return;
          }
  
          // Inform connections that data has been disabled to ensure we turn off video capability
          // if this is an LTE call.
          for (ImsPhoneConnection conn : mConnections) {
              ImsCall imsCall = conn.getImsCall();
              boolean isLocalVideoCapable = enabled || (imsCall != null && imsCall.isWifiCall());
              conn.setLocalVideoCapable(isLocalVideoCapable);
          }
  
          int reasonCode;
          if (reason == DataEnabledSettings.REASON_POLICY_DATA_ENABLED) {
              reasonCode = ImsReasonInfo.CODE_DATA_LIMIT_REACHED;
          } else if (reason == DataEnabledSettings.REASON_USER_DATA_ENABLED) {
              reasonCode = ImsReasonInfo.CODE_DATA_DISABLED;
          } else {
              // Unexpected code, default to data disabled.
              reasonCode = ImsReasonInfo.CODE_DATA_DISABLED;
          }
  
          // Potentially send connection events so the InCall UI knows that video calls are being
          // downgraded due to data being enabled/disabled.
          maybeNotifyDataDisabled(enabled, reasonCode);
          // Handle video state changes required as a result of data being enabled/disabled.
          handleDataEnabledChange(enabled, reasonCode);
  
          // We do not want to update the ImsConfig for REASON_REGISTERED, since it can happen before
          // the carrier config has loaded and will deregister IMS.
          if (!mShouldUpdateImsConfigOnDisconnect
                  && reason != DataEnabledSettings.REASON_REGISTERED && mCarrierConfigLoaded) {
              // This will call into updateVideoCallFeatureValue and eventually all clients will be
              // asynchronously notified that the availability of VT over LTE has changed.
              if (mImsManager != null) {
                  mImsManager.updateImsServiceConfig(true);
              }
          }
      }
  
      private void maybeNotifyDataDisabled(boolean enabled, int reasonCode) {
          if (!enabled) {
              // If data is disabled while there are ongoing VT calls which are not taking place over
              // wifi, then they should be disconnected to prevent the user from incurring further
              // data charges.
              for (ImsPhoneConnection conn : mConnections) {
                  ImsCall imsCall = conn.getImsCall();
                  if (imsCall != null && imsCall.isVideoCall() && !imsCall.isWifiCall()) {
                      if (conn.hasCapabilities(
                              Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_LOCAL |
                                      Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_REMOTE)) {
  
                          // If the carrier supports downgrading to voice, then we can simply issue a
                          // downgrade to voice instead of terminating the call.
                          if (reasonCode == ImsReasonInfo.CODE_DATA_DISABLED) {
                              conn.onConnectionEvent(TelephonyManager.EVENT_DOWNGRADE_DATA_DISABLED,
                                      null);
                          } else if (reasonCode == ImsReasonInfo.CODE_DATA_LIMIT_REACHED) {
                              conn.onConnectionEvent(
                                      TelephonyManager.EVENT_DOWNGRADE_DATA_LIMIT_REACHED, null);
                          }
                      }
                  }
              }
          }
      }
  
      /**
       * Handles changes to the enabled state of mobile data.
       * When data is disabled, handles auto-downgrade of video calls over LTE.
       * When data is enabled, handled resuming of video calls paused when data was disabled.
       * @param enabled {@code true} if mobile data is enabled, {@code false} if mobile data is
       *                            disabled.
       * @param reasonCode The {@link ImsReasonInfo} code for the data enabled state change.
       */
      private void handleDataEnabledChange(boolean enabled, int reasonCode) {
          if (!enabled) {
              // If data is disabled while there are ongoing VT calls which are not taking place over
              // wifi, then they should be disconnected to prevent the user from incurring further
              // data charges.
              for (ImsPhoneConnection conn : mConnections) {
                  ImsCall imsCall = conn.getImsCall();
                  if (imsCall != null && imsCall.isVideoCall() && !imsCall.isWifiCall()) {
                      log("handleDataEnabledChange - downgrading " + conn);
                      downgradeVideoCall(reasonCode, conn);
                  }
              }
          } else if (mSupportPauseVideo) {
              // Data was re-enabled, so un-pause previously paused video calls.
              for (ImsPhoneConnection conn : mConnections) {
                  // If video is paused, check to see if there are any pending pauses due to enabled
                  // state of data changing.
                  log("handleDataEnabledChange - resuming " + conn);
                  if (VideoProfile.isPaused(conn.getVideoState()) &&
                          conn.wasVideoPausedFromSource(VideoPauseTracker.SOURCE_DATA_ENABLED)) {
                      // The data enabled state was a cause of a pending pause, so potentially
                      // resume the video now.
                      conn.resumeVideo(VideoPauseTracker.SOURCE_DATA_ENABLED);
                  }
              }
              mShouldUpdateImsConfigOnDisconnect = false;
          }
      }
  
      /**
       * Handles downgrading a video call.  The behavior depends on carrier capabilities; we will
       * attempt to take one of the following actions (in order of precedence):
       * 1. If supported by the carrier, the call will be downgraded to an audio-only call.
       * 2. If the carrier supports video pause signalling, the video will be paused.
       * 3. The call will be disconnected.
       * @param reasonCode The {@link ImsReasonInfo} reason code for the downgrade.
       * @param conn The {@link ImsPhoneConnection} to downgrade.
       */
      private void downgradeVideoCall(int reasonCode, ImsPhoneConnection conn) {
          ImsCall imsCall = conn.getImsCall();
          if (imsCall != null) {
              if (conn.hasCapabilities(
                      Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_LOCAL |
                              Connection.Capability.SUPPORTS_DOWNGRADE_TO_VOICE_REMOTE)
                              && !mSupportPauseVideo) {
                  log("downgradeVideoCall :: callId=" + conn.getTelecomCallId()
                          + " Downgrade to audio");
                  // If the carrier supports downgrading to voice, then we can simply issue a
                  // downgrade to voice instead of terminating the call.
                  modifyVideoCall(imsCall, VideoProfile.STATE_AUDIO_ONLY);
              } else if (mSupportPauseVideo && reasonCode != ImsReasonInfo.CODE_WIFI_LOST) {
                  // The carrier supports video pause signalling, so pause the video if we didn't just
                  // lose wifi; in that case just disconnect.
                  log("downgradeVideoCall :: callId=" + conn.getTelecomCallId()
                          + " Pause audio");
                  mShouldUpdateImsConfigOnDisconnect = true;
                  conn.pauseVideo(VideoPauseTracker.SOURCE_DATA_ENABLED);
              } else {
                  log("downgradeVideoCall :: callId=" + conn.getTelecomCallId()
                          + " Disconnect call.");
                  // At this point the only choice we have is to terminate the call.
                  imsCall.terminate(ImsReasonInfo.CODE_USER_TERMINATED, reasonCode);
              }
          }
      }
  
      private void resetImsCapabilities() {
          log("Resetting Capabilities...");
          boolean tmpIsVideoCallEnabled = isVideoCallEnabled();
          mMmTelCapabilities = new MmTelFeature.MmTelCapabilities();
  
          boolean isVideoEnabled = isVideoCallEnabled();
          if (tmpIsVideoCallEnabled != isVideoEnabled) {
              mPhone.notifyForVideoCapabilityChanged(isVideoEnabled);
          }
      }
  
      /**
       * @return {@code true} if the device is connected to a WIFI network, {@code false} otherwise.
       */
      private boolean isWifiConnected() {
          ConnectivityManager cm = (ConnectivityManager) mPhone.getContext()
                  .getSystemService(Context.CONNECTIVITY_SERVICE);
          if (cm != null) {
              NetworkInfo ni = cm.getActiveNetworkInfo();
              if (ni != null && ni.isConnected()) {
                  return ni.getType() == ConnectivityManager.TYPE_WIFI;
              }
          }
          return false;
      }
  
      /**
       * Registers for changes to network connectivity.  Specifically requests the availability of new
       * WIFI networks which an IMS video call could potentially hand over to.
       */
      private void registerForConnectivityChanges() {
          if (mIsMonitoringConnectivity || !mNotifyVtHandoverToWifiFail) {
              return;
          }
          ConnectivityManager cm = (ConnectivityManager) mPhone.getContext()
                  .getSystemService(Context.CONNECTIVITY_SERVICE);
          if (cm != null) {
              Rlog.i(LOG_TAG, "registerForConnectivityChanges");
              NetworkCapabilities capabilities = new NetworkCapabilities();
              capabilities.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
              NetworkRequest.Builder builder = new NetworkRequest.Builder();
              builder.setCapabilities(capabilities);
              cm.registerNetworkCallback(builder.build(), mNetworkCallback);
              mIsMonitoringConnectivity = true;
          }
      }
  
      /**
       * Unregister for connectivity changes.  Will be called when a call disconnects or if the call
       * ends up handing over to WIFI.
       */
      private void unregisterForConnectivityChanges() {
          if (!mIsMonitoringConnectivity || !mNotifyVtHandoverToWifiFail) {
              return;
          }
          ConnectivityManager cm = (ConnectivityManager) mPhone.getContext()
                  .getSystemService(Context.CONNECTIVITY_SERVICE);
          if (cm != null) {
              Rlog.i(LOG_TAG, "unregisterForConnectivityChanges");
              cm.unregisterNetworkCallback(mNetworkCallback);
              mIsMonitoringConnectivity = false;
          }
      }
  
      /**
       * If the foreground call is a video call, schedule a handover check if one is not already
       * scheduled.  This method is intended ONLY for use when scheduling to watch for mid-call
       * handovers.
       */
      private void scheduleHandoverCheck() {
          ImsCall fgCall = mForegroundCall.getImsCall();
          ImsPhoneConnection conn = mForegroundCall.getFirstConnection();
          if (!mNotifyVtHandoverToWifiFail || fgCall == null || !fgCall.isVideoCall() || conn == null
                  || conn.getDisconnectCause() != DisconnectCause.NOT_DISCONNECTED) {
              return;
          }
  
          if (!hasMessages(EVENT_CHECK_FOR_WIFI_HANDOVER)) {
              Rlog.i(LOG_TAG, "scheduleHandoverCheck: schedule");
              sendMessageDelayed(obtainMessage(EVENT_CHECK_FOR_WIFI_HANDOVER, fgCall),
                      HANDOVER_TO_WIFI_TIMEOUT_MS);
          }
      }
  
      /**
       * @return {@code true} if downgrading of a video call to audio is supported.
           */
      public boolean isCarrierDowngradeOfVtCallSupported() {
          return mSupportDowngradeVtToAudio;
      }
  
      @VisibleForTesting
      public void setDataEnabled(boolean isDataEnabled) {
          mIsDataEnabled = isDataEnabled;
      }
  
      // Removes old call quality metrics if mCallQualityMetricsHistory exceeds its max size
      private void pruneCallQualityMetricsHistory() {
          if (mCallQualityMetricsHistory.size() > MAX_CALL_QUALITY_HISTORY) {
              mCallQualityMetricsHistory.poll();
          }
      }
  
      private void handleFeatureCapabilityChanged(ImsFeature.Capabilities capabilities) {
          boolean tmpIsVideoCallEnabled = isVideoCallEnabled();
          // Check enabledFeatures to determine capabilities. We ignore disabledFeatures.
          StringBuilder sb;
          if (DBG) {
              sb = new StringBuilder(120);
              sb.append("handleFeatureCapabilityChanged: ");
          }
          sb.append(capabilities);
          mMmTelCapabilities = new MmTelFeature.MmTelCapabilities(capabilities);
  
          boolean isVideoEnabled = isVideoCallEnabled();
          boolean isVideoEnabledStatechanged = tmpIsVideoCallEnabled != isVideoEnabled;
          if (DBG) {
              sb.append(" isVideoEnabledStateChanged=");
              sb.append(isVideoEnabledStatechanged);
          }
  
          if (isVideoEnabledStatechanged) {
              log("handleFeatureCapabilityChanged - notifyForVideoCapabilityChanged="
                      + isVideoEnabled);
              mPhone.notifyForVideoCapabilityChanged(isVideoEnabled);
          }
  
          if (DBG) log(sb.toString());
  
          if (DBG) {
              log("handleFeatureCapabilityChanged: isVolteEnabled=" + isVolteEnabled()
                      + ", isVideoCallEnabled=" + isVideoCallEnabled()
                      + ", isVowifiEnabled=" + isVowifiEnabled()
                      + ", isUtEnabled=" + isUtEnabled());
          }
  
          mPhone.onFeatureCapabilityChanged();
  
          mMetrics.writeOnImsCapabilities(mPhone.getPhoneId(), getImsRegistrationTech(),
                  mMmTelCapabilities);
      }
  
      @VisibleForTesting
      public void onCallHoldReceived(ImsCall imsCall) {
          if (DBG) log("onCallHoldReceived");
  
          ImsPhoneConnection conn = findConnection(imsCall);
          if (conn != null) {
              if (!mOnHoldToneStarted && (ImsPhoneCall.isLocalTone(imsCall)
                      || mAlwaysPlayRemoteHoldTone) &&
                      conn.getState() == ImsPhoneCall.State.ACTIVE) {
                  mPhone.startOnHoldTone(conn);
                  mOnHoldToneStarted = true;
                  mOnHoldToneId = System.identityHashCode(conn);
              }
              conn.onConnectionEvent(android.telecom.Connection.EVENT_CALL_REMOTELY_HELD, null);
  
              boolean useVideoPauseWorkaround = mPhone.getContext().getResources().getBoolean(
                      com.android.internal.R.bool.config_useVideoPauseWorkaround);
              if (useVideoPauseWorkaround && mSupportPauseVideo &&
                      VideoProfile.isVideo(conn.getVideoState())) {
                  // If we are using the video pause workaround, the vendor IMS code has issues
                  // with video pause signalling.  In this case, when a call is remotely
                  // held, the modem does not reliably change the video state of the call to be
                  // paused.
                  // As a workaround, we will turn on that bit now.
                  conn.changeToPausedState();
              }
          }
  
          SuppServiceNotification supp = new SuppServiceNotification();
          supp.notificationType = SuppServiceNotification.NOTIFICATION_TYPE_CODE_2;
          supp.code = SuppServiceNotification.CODE_2_CALL_ON_HOLD;
          mPhone.notifySuppSvcNotification(supp);
          mMetrics.writeOnImsCallHoldReceived(mPhone.getPhoneId(), imsCall.getCallSession());
      }
  
      @VisibleForTesting
      public void setAlwaysPlayRemoteHoldTone(boolean shouldPlayRemoteHoldTone) {
          mAlwaysPlayRemoteHoldTone = shouldPlayRemoteHoldTone;
      }
  
      private String getNetworkCountryIso() {
          String countryIso = "";
          if (mPhone != null) {
              ServiceStateTracker sst = mPhone.getServiceStateTracker();
              if (sst != null) {
                  LocaleTracker lt = sst.getLocaleTracker();
                  if (lt != null) {
                      countryIso = lt.getCurrentCountry();
                  }
              }
          }
          return countryIso;
      }
  
      @Override
      public ImsPhone getPhone() {
          return mPhone;
      }
  }
  