content: /*
   * Copyright (C) 2015 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;
  
  import android.annotation.UnsupportedAppUsage;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  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.SystemProperties;
  import android.telecom.TelecomManager;
  import android.telephony.CarrierConfigManager;
  import android.telephony.CellLocation;
  import android.telephony.DisconnectCause;
  import android.telephony.PhoneNumberUtils;
  import android.telephony.Rlog;
  import android.telephony.ServiceState;
  import android.telephony.TelephonyManager;
  import android.telephony.cdma.CdmaCellLocation;
  import android.telephony.gsm.GsmCellLocation;
  import android.text.TextUtils;
  import android.util.EventLog;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.telephony.cdma.CdmaCallWaitingNotification;
  import com.android.internal.telephony.metrics.TelephonyMetrics;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.Iterator;
  import java.util.List;
  
  /**
   * {@hide}
   */
  public class GsmCdmaCallTracker extends CallTracker {
      private static final String LOG_TAG = "GsmCdmaCallTracker";
      private static final boolean REPEAT_POLLING = false;
  
      private static final boolean DBG_POLL = false;
      private static final boolean VDBG = false;
  
      //***** Constants
  
      public static final int MAX_CONNECTIONS_GSM = 19;   //7 allowed in GSM + 12 from IMS for SRVCC
      private static final int MAX_CONNECTIONS_PER_CALL_GSM = 5; //only 5 connections allowed per call
  
      private static final int MAX_CONNECTIONS_CDMA = 8;
      private static final int MAX_CONNECTIONS_PER_CALL_CDMA = 1; //only 1 connection allowed per call
  
      //***** Instance Variables
      @VisibleForTesting
      public GsmCdmaConnection[] mConnections;
      private RegistrantList mVoiceCallEndedRegistrants = new RegistrantList();
      private RegistrantList mVoiceCallStartedRegistrants = new RegistrantList();
  
      // connections dropped during last poll
      private ArrayList<GsmCdmaConnection> mDroppedDuringPoll =
              new ArrayList<GsmCdmaConnection>(MAX_CONNECTIONS_GSM);
  
      @UnsupportedAppUsage
      public GsmCdmaCall mRingingCall = new GsmCdmaCall(this);
      // A call that is ringing or (call) waiting
      @UnsupportedAppUsage
      public GsmCdmaCall mForegroundCall = new GsmCdmaCall(this);
      @UnsupportedAppUsage
      public GsmCdmaCall mBackgroundCall = new GsmCdmaCall(this);
  
      @UnsupportedAppUsage
      private GsmCdmaConnection mPendingMO;
      private boolean mHangupPendingMO;
  
      @UnsupportedAppUsage
      private GsmCdmaPhone mPhone;
  
      private boolean mDesiredMute = false;    // false = mute off
  
      @UnsupportedAppUsage
      public PhoneConstants.State mState = PhoneConstants.State.IDLE;
  
      private TelephonyMetrics mMetrics = TelephonyMetrics.getInstance();
  
      // Following member variables are for CDMA only
      private RegistrantList mCallWaitingRegistrants = new RegistrantList();
      private boolean mPendingCallInEcm;
      private boolean mIsInEmergencyCall;
      private int mPendingCallClirMode;
      private boolean mIsEcmTimerCanceled;
      private int m3WayCallFlashDelay;
  
      /**
       * Listens for Emergency Callback Mode state change intents
       */
      private BroadcastReceiver mEcmExitReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              if (intent.getAction().equals(
                      TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED)) {
  
                  boolean isInEcm = intent.getBooleanExtra(PhoneConstants.PHONE_IN_ECM_STATE, false);
                  log("Received ACTION_EMERGENCY_CALLBACK_MODE_CHANGED isInEcm = " + isInEcm);
  
                  // If we exit ECM mode, notify all connections.
                  if (!isInEcm) {
                      // Although mConnections seems to be the place to look, it is not guaranteed
                      // to have all of the connections we're tracking.  THe best place to look is in
                      // the Call objects associated with the tracker.
                      List<Connection> toNotify = new ArrayList<Connection>();
                      toNotify.addAll(mRingingCall.getConnections());
                      toNotify.addAll(mForegroundCall.getConnections());
                      toNotify.addAll(mBackgroundCall.getConnections());
                      if (mPendingMO != null) {
                          toNotify.add(mPendingMO);
                      }
  
                      // Notify connections that ECM mode exited.
                      for (Connection connection : toNotify) {
                          if (connection != null) {
                              connection.onExitedEcmMode();
                          }
                      }
                  }
              }
          }
      };
  
      //***** Events
  
  
      //***** Constructors
  
      public GsmCdmaCallTracker (GsmCdmaPhone phone) {
          this.mPhone = phone;
          mCi = phone.mCi;
          mCi.registerForCallStateChanged(this, EVENT_CALL_STATE_CHANGE, null);
          mCi.registerForOn(this, EVENT_RADIO_AVAILABLE, null);
          mCi.registerForNotAvailable(this, EVENT_RADIO_NOT_AVAILABLE, null);
  
          // Register receiver for ECM exit
          IntentFilter filter = new IntentFilter();
          filter.addAction(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
          mPhone.getContext().registerReceiver(mEcmExitReceiver, filter);
  
          updatePhoneType(true);
      }
  
      public void updatePhoneType() {
          updatePhoneType(false);
      }
  
      private void updatePhoneType(boolean duringInit) {
          if (!duringInit) {
              reset();
              pollCallsWhenSafe();
          }
          if (mPhone.isPhoneTypeGsm()) {
              mConnections = new GsmCdmaConnection[MAX_CONNECTIONS_GSM];
              mCi.unregisterForCallWaitingInfo(this);
              // Prior to phone switch to GSM, if CDMA has any emergency call
              // data will be in disabled state, after switching to GSM enable data.
              if (mIsInEmergencyCall) {
                  mPhone.getDataEnabledSettings().setInternalDataEnabled(true);
              }
          } else {
              mConnections = new GsmCdmaConnection[MAX_CONNECTIONS_CDMA];
              mPendingCallInEcm = false;
              mIsInEmergencyCall = false;
              mPendingCallClirMode = CommandsInterface.CLIR_DEFAULT;
              mIsEcmTimerCanceled = false;
              m3WayCallFlashDelay = 0;
              mCi.registerForCallWaitingInfo(this, EVENT_CALL_WAITING_INFO_CDMA, null);
          }
      }
  
      private void reset() {
          Rlog.d(LOG_TAG, "reset");
  
          for (GsmCdmaConnection gsmCdmaConnection : mConnections) {
              if (gsmCdmaConnection != null) {
                  gsmCdmaConnection.onDisconnect(DisconnectCause.ERROR_UNSPECIFIED);
                  gsmCdmaConnection.dispose();
              }
          }
  
          if (mPendingMO != null) {
              // Send the notification that the pending call was disconnected to the higher layers.
              mPendingMO.onDisconnect(DisconnectCause.ERROR_UNSPECIFIED);
              mPendingMO.dispose();
          }
  
          mConnections = null;
          mPendingMO = null;
          clearDisconnected();
      }
  
      @Override
      protected void finalize() {
          Rlog.d(LOG_TAG, "GsmCdmaCallTracker 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);
          // Notify if in call when registering
          if (mState != PhoneConstants.State.IDLE) {
              r.notifyRegistrant(new AsyncResult(null, null, null));
          }
      }
  
      @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 void registerForCallWaiting(Handler h, int what, Object obj) {
          Registrant r = new Registrant (h, what, obj);
          mCallWaitingRegistrants.add(r);
      }
  
      public void unregisterForCallWaiting(Handler h) {
          mCallWaitingRegistrants.remove(h);
      }
  
      @UnsupportedAppUsage
      private void fakeHoldForegroundBeforeDial() {
          List<Connection> connCopy;
  
          // We need to make a copy here, since fakeHoldBeforeDial()
          // modifies the lists, and we don't want to reverse the order
          connCopy = (List<Connection>) mForegroundCall.mConnections.clone();
  
          for (int i = 0, s = connCopy.size() ; i < s ; i++) {
              GsmCdmaConnection conn = (GsmCdmaConnection)connCopy.get(i);
  
              conn.fakeHoldBeforeDial();
          }
      }
  
      //GSM
      /**
       * clirMode is one of the CLIR_ constants
       */
      public synchronized Connection dialGsm(String dialString, int clirMode, UUSInfo uusInfo,
                                          Bundle intentExtras)
              throws CallStateException {
          // note that this triggers call state changed notif
          clearDisconnected();
  
          // Check for issues which would preclude dialing and throw a CallStateException.
          boolean isEmergencyCall = PhoneNumberUtils.isLocalEmergencyNumber(mPhone.getContext(),
                  dialString);
          checkForDialIssues(isEmergencyCall);
  
          String origNumber = dialString;
          dialString = convertNumberIfNecessary(mPhone, dialString);
  
          // 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() == GsmCdmaCall.State.ACTIVE) {
              // this will probably be done by the radio anyway
              // but the dial might fail before this happens
              // and we need to make sure the foreground call is clear
              // for the newly dialed connection
              switchWaitingOrHoldingAndActive();
              // This is a hack to delay DIAL so that it is sent out to RIL only after
              // EVENT_SWITCH_RESULT is received. We've seen failures when adding a new call to
              // multi-way conference calls due to DIAL being sent out before SWITCH is processed
              try {
                  Thread.sleep(500);
              } catch (InterruptedException e) {
                  // do nothing
              }
  
              // Fake local state so that
              // a) foregroundCall is empty for the newly dialed connection
              // b) hasNonHangupStateChanged remains false in the
              // next poll, so that we don't clear a failed dialing call
              fakeHoldForegroundBeforeDial();
          }
  
          if (mForegroundCall.getState() != GsmCdmaCall.State.IDLE) {
              //we should have failed in !canDial() above before we get here
              throw new CallStateException("cannot dial in current state");
          }
  
          mPendingMO = new GsmCdmaConnection(mPhone, checkForTestEmergencyNumber(dialString),
                  this, mForegroundCall, isEmergencyCall);
          if (intentExtras != null) {
              Rlog.d(LOG_TAG, "dialGsm - emergency dialer: " + intentExtras.getBoolean(
                      TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL));
              mPendingMO.setHasKnownUserIntentEmergency(intentExtras.getBoolean(
                      TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL));
          }
          mHangupPendingMO = false;
          mMetrics.writeRilDial(mPhone.getPhoneId(), mPendingMO, clirMode, uusInfo);
  
  
          if ( mPendingMO.getAddress() == null || mPendingMO.getAddress().length() == 0
                  || mPendingMO.getAddress().indexOf(PhoneNumberUtils.WILD) >= 0) {
              // Phone number is invalid
              mPendingMO.mCause = DisconnectCause.INVALID_NUMBER;
  
              // handlePollCalls() will notice this call not present
              // and will mark it as dropped.
              pollCallsWhenSafe();
          } else {
              // Always unmute when initiating a new call
              setMute(false);
  
              mCi.dial(mPendingMO.getAddress(), mPendingMO.isEmergencyCall(),
                      mPendingMO.getEmergencyNumberInfo(), mPendingMO.hasKnownUserIntentEmergency(),
                      clirMode, uusInfo, obtainCompleteMessage());
          }
  
          if (mNumberConverted) {
              mPendingMO.setConverted(origNumber);
              mNumberConverted = false;
          }
  
          updatePhoneState();
          mPhone.notifyPreciseCallStateChanged();
  
          return mPendingMO;
      }
  
      //CDMA
      /**
       * Handle Ecm timer to be canceled or re-started
       */
      @UnsupportedAppUsage
      private void handleEcmTimer(int action) {
          mPhone.handleTimerInEmergencyCallbackMode(action);
          switch(action) {
              case GsmCdmaPhone.CANCEL_ECM_TIMER: mIsEcmTimerCanceled = true; break;
              case GsmCdmaPhone.RESTART_ECM_TIMER: mIsEcmTimerCanceled = false; break;
              default:
                  Rlog.e(LOG_TAG, "handleEcmTimer, unsupported action " + action);
          }
      }
  
      //CDMA
      /**
       * Disable data call when emergency call is connected
       */
      @UnsupportedAppUsage
      private void disableDataCallInEmergencyCall(String dialString) {
          if (PhoneNumberUtils.isLocalEmergencyNumber(mPhone.getContext(), dialString)) {
              if (Phone.DEBUG_PHONE) log("disableDataCallInEmergencyCall");
              setIsInEmergencyCall();
          }
      }
  
      //CDMA
      public void setIsInEmergencyCall() {
          mIsInEmergencyCall = true;
          mPhone.getDataEnabledSettings().setInternalDataEnabled(false);
          mPhone.notifyEmergencyCallRegistrants(true);
          mPhone.sendEmergencyCallStateChange(true);
      }
  
      //CDMA
      /**
       * clirMode is one of the CLIR_ constants
       */
      private Connection dialCdma(String dialString, int clirMode, Bundle intentExtras)
              throws CallStateException {
          // note that this triggers call state changed notif
          clearDisconnected();
  
          boolean isEmergencyCall =
                  PhoneNumberUtils.isLocalEmergencyNumber(mPhone.getContext(), dialString);
  
          // Check for issues which would preclude dialing and throw a CallStateException.
          checkForDialIssues(isEmergencyCall);
  
          TelephonyManager tm =
                  (TelephonyManager) mPhone.getContext().getSystemService(Context.TELEPHONY_SERVICE);
          String origNumber = dialString;
          String operatorIsoContry = tm.getNetworkCountryIsoForPhone(mPhone.getPhoneId());
          String simIsoContry = tm.getSimCountryIsoForPhone(mPhone.getPhoneId());
          boolean internationalRoaming = !TextUtils.isEmpty(operatorIsoContry)
                  && !TextUtils.isEmpty(simIsoContry)
                  && !simIsoContry.equals(operatorIsoContry);
          if (internationalRoaming) {
              if ("us".equals(simIsoContry)) {
                  internationalRoaming = internationalRoaming && !"vi".equals(operatorIsoContry);
              } else if ("vi".equals(simIsoContry)) {
                  internationalRoaming = internationalRoaming && !"us".equals(operatorIsoContry);
              }
          }
          if (internationalRoaming) {
              dialString = convertNumberIfNecessary(mPhone, dialString);
          }
  
          boolean isPhoneInEcmMode = mPhone.isInEcm();
  
          // Cancel Ecm timer if a second emergency call is originating in Ecm mode
          if (isPhoneInEcmMode && isEmergencyCall) {
              handleEcmTimer(GsmCdmaPhone.CANCEL_ECM_TIMER);
          }
  
          // 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() == GsmCdmaCall.State.ACTIVE) {
              return dialThreeWay(dialString, intentExtras);
          }
  
          mPendingMO = new GsmCdmaConnection(mPhone, checkForTestEmergencyNumber(dialString),
                  this, mForegroundCall, isEmergencyCall);
          if (intentExtras != null) {
              Rlog.d(LOG_TAG, "dialGsm - emergency dialer: " + intentExtras.getBoolean(
                      TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL));
              mPendingMO.setHasKnownUserIntentEmergency(intentExtras.getBoolean(
                      TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL));
          }
          mHangupPendingMO = false;
  
          if ( mPendingMO.getAddress() == null || mPendingMO.getAddress().length() == 0
                  || mPendingMO.getAddress().indexOf(PhoneNumberUtils.WILD) >= 0 ) {
              // Phone number is invalid
              mPendingMO.mCause = DisconnectCause.INVALID_NUMBER;
  
              // handlePollCalls() will notice this call not present
              // and will mark it as dropped.
              pollCallsWhenSafe();
          } else {
              // Always unmute when initiating a new call
              setMute(false);
  
              // Check data call
              disableDataCallInEmergencyCall(dialString);
  
              // In Ecm mode, if another emergency call is dialed, Ecm mode will not exit.
              if(!isPhoneInEcmMode || (isPhoneInEcmMode && isEmergencyCall)) {
                  mCi.dial(mPendingMO.getAddress(), mPendingMO.isEmergencyCall(),
                          mPendingMO.getEmergencyNumberInfo(),
                          mPendingMO.hasKnownUserIntentEmergency(),
                          clirMode, obtainCompleteMessage());
              } else {
                  mPhone.exitEmergencyCallbackMode();
                  mPhone.setOnEcbModeExitResponse(this,EVENT_EXIT_ECM_RESPONSE_CDMA, null);
                  mPendingCallClirMode=clirMode;
                  mPendingCallInEcm=true;
              }
          }
  
          if (mNumberConverted) {
              mPendingMO.setConverted(origNumber);
              mNumberConverted = false;
          }
  
          updatePhoneState();
          mPhone.notifyPreciseCallStateChanged();
  
          return mPendingMO;
      }
  
      //CDMA
      private Connection dialThreeWay(String dialString, Bundle intentExtras) {
          if (!mForegroundCall.isIdle()) {
              // Check data call and possibly set mIsInEmergencyCall
              disableDataCallInEmergencyCall(dialString);
  
              // Attach the new connection to foregroundCall
              mPendingMO = new GsmCdmaConnection(mPhone,
                      checkForTestEmergencyNumber(dialString), this, mForegroundCall,
                      mIsInEmergencyCall);
              if (intentExtras != null) {
                  Rlog.d(LOG_TAG, "dialThreeWay - emergency dialer " + intentExtras.getBoolean(
                          TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL));
                  mPendingMO.setHasKnownUserIntentEmergency(intentExtras.getBoolean(
                          TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL));
              }
              // Some networks need an empty flash before sending the normal one
              CarrierConfigManager configManager = (CarrierConfigManager)
                      mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
              PersistableBundle bundle = configManager.getConfigForSubId(mPhone.getSubId());
              if (bundle != null) {
                  m3WayCallFlashDelay =
                          bundle.getInt(CarrierConfigManager.KEY_CDMA_3WAYCALL_FLASH_DELAY_INT);
              } else {
                  // The default 3-way call flash delay is 0s
                  m3WayCallFlashDelay = 0;
              }
              if (m3WayCallFlashDelay > 0) {
                  mCi.sendCDMAFeatureCode("", obtainMessage(EVENT_THREE_WAY_DIAL_BLANK_FLASH));
              } else {
                  mCi.sendCDMAFeatureCode(mPendingMO.getAddress(),
                          obtainMessage(EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA));
              }
              return mPendingMO;
          }
          return null;
      }
  
      public Connection dial(String dialString, Bundle intentExtras) throws CallStateException {
          if (isPhoneTypeGsm()) {
              return dialGsm(dialString, CommandsInterface.CLIR_DEFAULT, intentExtras);
          } else {
              return dialCdma(dialString, CommandsInterface.CLIR_DEFAULT, intentExtras);
          }
      }
  
      //GSM
      public Connection dialGsm(String dialString, UUSInfo uusInfo, Bundle intentExtras)
              throws CallStateException {
          return dialGsm(dialString, CommandsInterface.CLIR_DEFAULT, uusInfo, intentExtras);
      }
  
      //GSM
      private Connection dialGsm(String dialString, int clirMode, Bundle intentExtras)
              throws CallStateException {
          return dialGsm(dialString, clirMode, null, intentExtras);
      }
  
      public void acceptCall() throws CallStateException {
          // FIXME if SWITCH fails, should retry with ANSWER
          // in case the active/holding call disappeared and this
          // is no longer call waiting
  
          if (mRingingCall.getState() == GsmCdmaCall.State.INCOMING) {
              Rlog.i("phone", "acceptCall: incoming...");
              // Always unmute when answering a new call
              setMute(false);
              mCi.acceptCall(obtainCompleteMessage());
          } else if (mRingingCall.getState() == GsmCdmaCall.State.WAITING) {
              if (isPhoneTypeGsm()) {
                  setMute(false);
              } else {
                  GsmCdmaConnection cwConn = (GsmCdmaConnection)(mRingingCall.getLatestConnection());
  
                  // Since there is no network response for supplimentary
                  // service for CDMA, we assume call waiting is answered.
                  // ringing Call state change to idle is in GsmCdmaCall.detach
                  // triggered by updateParent.
                  cwConn.updateParent(mRingingCall, mForegroundCall);
                  cwConn.onConnectedInOrOut();
                  updatePhoneState();
              }
              switchWaitingOrHoldingAndActive();
          } else {
              throw new CallStateException("phone not ringing");
          }
      }
  
      public void rejectCall() throws CallStateException {
          // AT+CHLD=0 means "release held or UDUB"
          // so if the phone isn't ringing, this could hang up held
          if (mRingingCall.getState().isRinging()) {
              mCi.rejectCall(obtainCompleteMessage());
          } else {
              throw new CallStateException("phone not ringing");
          }
      }
  
      //CDMA
      private void flashAndSetGenericTrue() {
          mCi.sendCDMAFeatureCode("", obtainMessage(EVENT_SWITCH_RESULT));
  
          mPhone.notifyPreciseCallStateChanged();
      }
  
      @UnsupportedAppUsage
      public void switchWaitingOrHoldingAndActive() throws CallStateException {
          // Should we bother with this check?
          if (mRingingCall.getState() == GsmCdmaCall.State.INCOMING) {
              throw new CallStateException("cannot be in the incoming state");
          } else {
              if (isPhoneTypeGsm()) {
                  mCi.switchWaitingOrHoldingAndActive(
                          obtainCompleteMessage(EVENT_SWITCH_RESULT));
              } else {
                  if (mForegroundCall.getConnections().size() > 1) {
                      flashAndSetGenericTrue();
                  } else {
                      // Send a flash command to CDMA network for putting the other party on hold.
                      // For CDMA networks which do not support this the user would just hear a beep
                      // from the network. For CDMA networks which do support it will put the other
                      // party on hold.
                      mCi.sendCDMAFeatureCode("", obtainMessage(EVENT_SWITCH_RESULT));
                  }
              }
          }
      }
  
      public void conference() {
          if (isPhoneTypeGsm()) {
              mCi.conference(obtainCompleteMessage(EVENT_CONFERENCE_RESULT));
          } else {
              // Should we be checking state?
              flashAndSetGenericTrue();
          }
      }
  
      public void explicitCallTransfer() {
          mCi.explicitCallTransfer(obtainCompleteMessage(EVENT_ECT_RESULT));
      }
  
      @UnsupportedAppUsage
      public void clearDisconnected() {
          internalClearDisconnected();
  
          updatePhoneState();
          mPhone.notifyPreciseCallStateChanged();
      }
  
      public boolean canConference() {
          return mForegroundCall.getState() == GsmCdmaCall.State.ACTIVE
                  && mBackgroundCall.getState() == GsmCdmaCall.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(boolean isEmergencyCall) throws CallStateException {
          String disableCall = SystemProperties.get(
                  TelephonyProperties.PROPERTY_DISABLE_CALL, "false");
  
          if (mCi.getRadioState() != TelephonyManager.RADIO_POWER_ON) {
              throw new CallStateException(CallStateException.ERROR_POWER_OFF,
                      "Modem not powered");
          }
          if (disableCall.equals("true")) {
              throw new CallStateException(CallStateException.ERROR_CALLING_DISABLED,
                      "Calling disabled via ro.telephony.disable-call property");
          }
          if (mPendingMO != null) {
              throw new CallStateException(CallStateException.ERROR_ALREADY_DIALING,
                      "A call is already dialing.");
          }
          if (mRingingCall.isRinging()) {
              throw new CallStateException(CallStateException.ERROR_CALL_RINGING,
                      "Can't call while a call is ringing.");
          }
          if (isPhoneTypeGsm()
                  && mForegroundCall.getState().isAlive() && mBackgroundCall.getState().isAlive()) {
              throw new CallStateException(CallStateException.ERROR_TOO_MANY_CALLS,
                      "There is already a foreground and background call.");
          }
          if (!isPhoneTypeGsm()
                  // Essentially foreground call state is one of:
                  // HOLDING, DIALING, ALERTING, INCOMING, WAITING
                  && mForegroundCall.getState().isAlive()
                  && mForegroundCall.getState() != GsmCdmaCall.State.ACTIVE
  
                  && mBackgroundCall.getState().isAlive()) {
              throw new CallStateException(CallStateException.ERROR_TOO_MANY_CALLS,
                      "There is already a foreground and background call.");
          }
          if (!isEmergencyCall && isInOtaspCall()) {
              throw new CallStateException(CallStateException.ERROR_OTASP_PROVISIONING_IN_PROCESS,
                      "OTASP provisioning is in process.");
          }
      }
  
      public boolean canTransfer() {
          if (isPhoneTypeGsm()) {
              return (mForegroundCall.getState() == GsmCdmaCall.State.ACTIVE
                      || mForegroundCall.getState() == GsmCdmaCall.State.ALERTING
                      || mForegroundCall.getState() == GsmCdmaCall.State.DIALING)
                      && mBackgroundCall.getState() == GsmCdmaCall.State.HOLDING;
          } else {
              Rlog.e(LOG_TAG, "canTransfer: not possible in CDMA");
              return false;
          }
      }
  
      //***** Private Instance Methods
  
      private void internalClearDisconnected() {
          mRingingCall.clearDisconnected();
          mForegroundCall.clearDisconnected();
          mBackgroundCall.clearDisconnected();
      }
  
      /**
       * Obtain a message to use for signalling "invoke getCurrentCalls() when
       * this operation and all other pending operations are complete
       */
      @UnsupportedAppUsage
      private Message obtainCompleteMessage() {
          return obtainCompleteMessage(EVENT_OPERATION_COMPLETE);
      }
  
      /**
       * Obtain a message to use for signalling "invoke getCurrentCalls() when
       * this operation and all other pending operations are complete
       */
      @UnsupportedAppUsage
      private Message obtainCompleteMessage(int what) {
          mPendingOperations++;
          mLastRelevantPoll = null;
          mNeedsPoll = true;
  
          if (DBG_POLL) log("obtainCompleteMessage: pendingOperations=" +
                  mPendingOperations + ", needsPoll=" + mNeedsPoll);
  
          return obtainMessage(what);
      }
  
      private void operationComplete() {
          mPendingOperations--;
  
          if (DBG_POLL) log("operationComplete: pendingOperations=" +
                  mPendingOperations + ", needsPoll=" + mNeedsPoll);
  
          if (mPendingOperations == 0 && mNeedsPoll) {
              mLastRelevantPoll = obtainMessage(EVENT_POLL_CALLS_RESULT);
              mCi.getCurrentCalls(mLastRelevantPoll);
          } else if (mPendingOperations < 0) {
              // this should never happen
              Rlog.e(LOG_TAG,"GsmCdmaCallTracker.pendingOperations < 0");
              mPendingOperations = 0;
          }
      }
  
      @UnsupportedAppUsage
      private void updatePhoneState() {
          PhoneConstants.State oldState = mState;
          if (mRingingCall.isRinging()) {
              mState = PhoneConstants.State.RINGING;
          } else if (mPendingMO != null ||
                  !(mForegroundCall.isIdle() && mBackgroundCall.isIdle())) {
              mState = PhoneConstants.State.OFFHOOK;
          } else {
              Phone imsPhone = mPhone.getImsPhone();
              if ( mState == PhoneConstants.State.OFFHOOK && (imsPhone != null)){
                  imsPhone.callEndCleanupHandOverCallIfAny();
              }
              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 (Phone.DEBUG_PHONE) {
              log("update phone state, old=" + oldState + " new="+ mState);
          }
          if (mState != oldState) {
              mPhone.notifyPhoneStateChanged();
              mMetrics.writePhoneState(mPhone.getPhoneId(), mState);
          }
      }
  
      // ***** Overwritten from CallTracker
  
      @Override
      protected synchronized void handlePollCalls(AsyncResult ar) {
          List polledCalls;
  
          if (VDBG) log("handlePollCalls");
          if (ar.exception == null) {
              polledCalls = (List)ar.result;
          } else if (isCommandExceptionRadioNotAvailable(ar.exception)) {
              // just a dummy empty ArrayList to cause the loop
              // to hang up all the calls
              polledCalls = new ArrayList();
          } else {
              // Radio probably wasn't ready--try again in a bit
              // But don't keep polling if the channel is closed
              pollCallsAfterDelay();
              return;
          }
  
          Connection newRinging = null; //or waiting
          ArrayList<Connection> newUnknownConnectionsGsm = new ArrayList<Connection>();
          Connection newUnknownConnectionCdma = null;
          boolean hasNonHangupStateChanged = false;   // Any change besides
                                                      // a dropped connection
          boolean hasAnyCallDisconnected = false;
          boolean needsPollDelay = false;
          boolean unknownConnectionAppeared = false;
          int handoverConnectionsSize = mHandoverConnections.size();
  
          //CDMA
          boolean noConnectionExists = true;
  
          for (int i = 0, curDC = 0, dcSize = polledCalls.size()
                  ; i < mConnections.length; i++) {
              GsmCdmaConnection conn = mConnections[i];
              DriverCall dc = null;
  
              // polledCall list is sparse
              if (curDC < dcSize) {
                  dc = (DriverCall) polledCalls.get(curDC);
  
                  if (dc.index == i+1) {
                      curDC++;
                  } else {
                      dc = null;
                  }
              }
  
              //CDMA
              if (conn != null || dc != null) {
                  noConnectionExists = false;
              }
  
              if (DBG_POLL) log("poll: conn[i=" + i + "]=" +
                      conn+", dc=" + dc);
  
              if (conn == null && dc != null) {
                  // Connection appeared in CLCC response that we don't know about
                  if (mPendingMO != null && mPendingMO.compareTo(dc)) {
  
                      if (DBG_POLL) log("poll: pendingMO=" + mPendingMO);
  
                      // It's our pending mobile originating call
                      mConnections[i] = mPendingMO;
                      mPendingMO.mIndex = i;
                      mPendingMO.update(dc);
                      mPendingMO = null;
  
                      // Someone has already asked to hangup this call
                      if (mHangupPendingMO) {
                          mHangupPendingMO = false;
  
                          // Re-start Ecm timer when an uncompleted emergency call ends
                          if (!isPhoneTypeGsm() && mIsEcmTimerCanceled) {
                              handleEcmTimer(GsmCdmaPhone.RESTART_ECM_TIMER);
                          }
  
                          try {
                              if (Phone.DEBUG_PHONE) log(
                                      "poll: hangupPendingMO, hangup conn " + i);
                              hangup(mConnections[i]);
                          } catch (CallStateException ex) {
                              Rlog.e(LOG_TAG, "unexpected error on hangup");
                          }
  
                          // Do not continue processing this poll
                          // Wait for hangup and repoll
                          return;
                      }
                  } else {
                      if (Phone.DEBUG_PHONE) {
                          log("pendingMo=" + mPendingMO + ", dc=" + dc);
                      }
  
                      mConnections[i] = new GsmCdmaConnection(mPhone, dc, this, i);
  
                      Connection hoConnection = getHoConnection(dc);
                      if (hoConnection != null) {
                          // Single Radio Voice Call Continuity (SRVCC) completed
                          mConnections[i].migrateFrom(hoConnection);
                          // Updating connect time for silent redial cases (ex: Calls are transferred
                          // from DIALING/ALERTING/INCOMING/WAITING to ACTIVE)
                          if (hoConnection.mPreHandoverState != GsmCdmaCall.State.ACTIVE &&
                                  hoConnection.mPreHandoverState != GsmCdmaCall.State.HOLDING &&
                                  dc.state == DriverCall.State.ACTIVE) {
                              mConnections[i].onConnectedInOrOut();
                          } else {
                              mConnections[i].onConnectedConnectionMigrated();
                          }
  
                          mHandoverConnections.remove(hoConnection);
  
                          if (isPhoneTypeGsm()) {
                              for (Iterator<Connection> it = mHandoverConnections.iterator();
                                   it.hasNext(); ) {
                                  Connection c = it.next();
                                  Rlog.i(LOG_TAG, "HO Conn state is " + c.mPreHandoverState);
                                  if (c.mPreHandoverState == mConnections[i].getState()) {
                                      Rlog.i(LOG_TAG, "Removing HO conn "
                                              + hoConnection + c.mPreHandoverState);
                                      it.remove();
                                  }
                              }
                          }
  
                          mPhone.notifyHandoverStateChanged(mConnections[i]);
                      } else {
                          // find if the MT call is a new ring or unknown connection
                          newRinging = checkMtFindNewRinging(dc,i);
                          if (newRinging == null) {
                              unknownConnectionAppeared = true;
                              if (isPhoneTypeGsm()) {
                                  newUnknownConnectionsGsm.add(mConnections[i]);
                              } else {
                                  newUnknownConnectionCdma = mConnections[i];
                              }
                          }
                      }
                  }
                  hasNonHangupStateChanged = true;
              } else if (conn != null && dc == null) {
                  if (isPhoneTypeGsm()) {
                      // Connection missing in CLCC response that we were
                      // tracking.
                      mDroppedDuringPoll.add(conn);
                  } else {
                      // This case means the RIL has no more active call anymore and
                      // we need to clean up the foregroundCall and ringingCall.
                      // Loop through foreground call connections as
                      // it contains the known logical connections.
                      int count = mForegroundCall.mConnections.size();
                      for (int n = 0; n < count; n++) {
                          if (Phone.DEBUG_PHONE) log("adding fgCall cn " + n + " to droppedDuringPoll");
                          GsmCdmaConnection cn = (GsmCdmaConnection)mForegroundCall.mConnections.get(n);
                          mDroppedDuringPoll.add(cn);
                      }
                      count = mRingingCall.mConnections.size();
                      // Loop through ringing call connections as
                      // it may contain the known logical connections.
                      for (int n = 0; n < count; n++) {
                          if (Phone.DEBUG_PHONE) log("adding rgCall cn " + n + " to droppedDuringPoll");
                          GsmCdmaConnection cn = (GsmCdmaConnection)mRingingCall.mConnections.get(n);
                          mDroppedDuringPoll.add(cn);
                      }
  
                      // Re-start Ecm timer when the connected emergency call ends
                      if (mIsEcmTimerCanceled) {
                          handleEcmTimer(GsmCdmaPhone.RESTART_ECM_TIMER);
                      }
                      // If emergency call is not going through while dialing
                      checkAndEnableDataCallAfterEmergencyCallDropped();
                  }
                  // Dropped connections are removed from the CallTracker
                  // list but kept in the Call list
                  mConnections[i] = null;
              } else if (conn != null && dc != null && !conn.compareTo(dc) && isPhoneTypeGsm()) {
                  // Connection in CLCC response does not match what
                  // we were tracking. Assume dropped call and new call
  
                  mDroppedDuringPoll.add(conn);
                  mConnections[i] = new GsmCdmaConnection (mPhone, dc, this, i);
  
                  if (mConnections[i].getCall() == mRingingCall) {
                      newRinging = mConnections[i];
                  } // else something strange happened
                  hasNonHangupStateChanged = true;
              } else if (conn != null && dc != null) { /* implicit conn.compareTo(dc) */
                  // Call collision case
                  if (!isPhoneTypeGsm() && conn.isIncoming() != dc.isMT) {
                      if (dc.isMT == true) {
                          // Mt call takes precedence than Mo,drops Mo
                          mDroppedDuringPoll.add(conn);
                          // find if the MT call is a new ring or unknown connection
                          newRinging = checkMtFindNewRinging(dc,i);
                          if (newRinging == null) {
                              unknownConnectionAppeared = true;
                              newUnknownConnectionCdma = conn;
                          }
                          checkAndEnableDataCallAfterEmergencyCallDropped();
                      } else {
                          // Call info stored in conn is not consistent with the call info from dc.
                          // We should follow the rule of MT calls taking precedence over MO calls
                          // when there is conflict, so here we drop the call info from dc and
                          // continue to use the call info from conn, and only take a log.
                          Rlog.e(LOG_TAG,"Error in RIL, Phantom call appeared " + dc);
                      }
                  } else {
                      boolean changed;
                      changed = conn.update(dc);
                      hasNonHangupStateChanged = hasNonHangupStateChanged || changed;
                  }
              }
  
              if (REPEAT_POLLING) {
                  if (dc != null) {
                      // FIXME with RIL, we should not need this anymore
                      if ((dc.state == DriverCall.State.DIALING
                              /*&& cm.getOption(cm.OPTION_POLL_DIALING)*/)
                          || (dc.state == DriverCall.State.ALERTING
                              /*&& cm.getOption(cm.OPTION_POLL_ALERTING)*/)
                          || (dc.state == DriverCall.State.INCOMING
                              /*&& cm.getOption(cm.OPTION_POLL_INCOMING)*/)
                          || (dc.state == DriverCall.State.WAITING
                              /*&& cm.getOption(cm.OPTION_POLL_WAITING)*/)) {
                          // Sometimes there's no unsolicited notification
                          // for state transitions
                          needsPollDelay = true;
                      }
                  }
              }
          }
  
          // Safety check so that obj is not stuck with mIsInEmergencyCall set to true (and data
          // disabled). This should never happen though.
          if (!isPhoneTypeGsm() && noConnectionExists) {
              checkAndEnableDataCallAfterEmergencyCallDropped();
          }
  
          // This is the first poll after an ATD.
          // We expect the pending call to appear in the list
          // If it does not, we land here
          if (mPendingMO != null) {
              Rlog.d(LOG_TAG, "Pending MO dropped before poll fg state:"
                      + mForegroundCall.getState());
  
              mDroppedDuringPoll.add(mPendingMO);
              mPendingMO = null;
              mHangupPendingMO = false;
  
              if (!isPhoneTypeGsm()) {
                  if( mPendingCallInEcm) {
                      mPendingCallInEcm = false;
                  }
                  checkAndEnableDataCallAfterEmergencyCallDropped();
              }
          }
  
          if (newRinging != null) {
              mPhone.notifyNewRingingConnection(newRinging);
          }
  
          // clear the "local hangup" and "missed/rejected call"
          // cases from the "dropped during poll" list
          // These cases need no "last call fail" reason
          ArrayList<GsmCdmaConnection> locallyDisconnectedConnections = new ArrayList<>();
          for (int i = mDroppedDuringPoll.size() - 1; i >= 0 ; i--) {
              GsmCdmaConnection conn = mDroppedDuringPoll.get(i);
              //CDMA
              boolean wasDisconnected = false;
  
              if (conn.isIncoming() && conn.getConnectTime() == 0) {
                  // Missed or rejected call
                  int cause;
                  if (conn.mCause == DisconnectCause.LOCAL) {
                      cause = DisconnectCause.INCOMING_REJECTED;
                  } else {
                      cause = DisconnectCause.INCOMING_MISSED;
                  }
  
                  if (Phone.DEBUG_PHONE) {
                      log("missed/rejected call, conn.cause=" + conn.mCause);
                      log("setting cause to " + cause);
                  }
                  mDroppedDuringPoll.remove(i);
                  hasAnyCallDisconnected |= conn.onDisconnect(cause);
                  wasDisconnected = true;
                  locallyDisconnectedConnections.add(conn);
              } else if (conn.mCause == DisconnectCause.LOCAL
                      || conn.mCause == DisconnectCause.INVALID_NUMBER) {
                  mDroppedDuringPoll.remove(i);
                  hasAnyCallDisconnected |= conn.onDisconnect(conn.mCause);
                  wasDisconnected = true;
                  locallyDisconnectedConnections.add(conn);
              }
  
              if (!isPhoneTypeGsm() && wasDisconnected && unknownConnectionAppeared
                      && conn == newUnknownConnectionCdma) {
                  unknownConnectionAppeared = false;
                  newUnknownConnectionCdma = null;
              }
          }
  
          if (locallyDisconnectedConnections.size() > 0) {
              mMetrics.writeRilCallList(mPhone.getPhoneId(), locallyDisconnectedConnections,
                      getNetworkCountryIso());
          }
  
          /* Disconnect any pending Handover connections */
          for (Iterator<Connection> it = mHandoverConnections.iterator();
                  it.hasNext();) {
              Connection hoConnection = it.next();
              log("handlePollCalls - disconnect hoConn= " + hoConnection +
                      " hoConn.State= " + hoConnection.getState());
              if (hoConnection.getState().isRinging()) {
                  hoConnection.onDisconnect(DisconnectCause.INCOMING_MISSED);
              } else {
                  hoConnection.onDisconnect(DisconnectCause.NOT_VALID);
              }
              // TODO: Do we need to update these hoConnections in Metrics ?
              it.remove();
          }
  
          // Any non-local disconnects: determine cause
          if (mDroppedDuringPoll.size() > 0) {
              mCi.getLastCallFailCause(
                  obtainNoPollCompleteMessage(EVENT_GET_LAST_CALL_FAIL_CAUSE));
          }
  
          if (needsPollDelay) {
              pollCallsAfterDelay();
          }
  
          // Cases when we can no longer keep disconnected Connection's
          // with their previous calls
          // 1) the phone has started to ring
          // 2) A Call/Connection object has changed state...
          //    we may have switched or held or answered (but not hung up)
          if (newRinging != null || hasNonHangupStateChanged || hasAnyCallDisconnected) {
              internalClearDisconnected();
          }
  
          if (VDBG) log("handlePollCalls calling updatePhoneState()");
          updatePhoneState();
  
          if (unknownConnectionAppeared) {
              if (isPhoneTypeGsm()) {
                  for (Connection c : newUnknownConnectionsGsm) {
                      log("Notify unknown for " + c);
                      mPhone.notifyUnknownConnection(c);
                  }
              } else {
                  mPhone.notifyUnknownConnection(newUnknownConnectionCdma);
              }
          }
  
          if (hasNonHangupStateChanged || newRinging != null || hasAnyCallDisconnected) {
              mPhone.notifyPreciseCallStateChanged();
              updateMetrics(mConnections);
          }
  
          // If all handover connections are mapped during this poll process clean it up
          if (handoverConnectionsSize > 0 && mHandoverConnections.size() == 0) {
              Phone imsPhone = mPhone.getImsPhone();
              if (imsPhone != null) {
                  imsPhone.callEndCleanupHandOverCallIfAny();
              }
          }
          //dumpState();
      }
  
      private void updateMetrics(GsmCdmaConnection[] connections) {
          ArrayList<GsmCdmaConnection> activeConnections = new ArrayList<>();
          for (GsmCdmaConnection conn : connections) {
              if (conn != null) activeConnections.add(conn);
          }
          mMetrics.writeRilCallList(mPhone.getPhoneId(), activeConnections, getNetworkCountryIso());
      }
  
      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;
  
          Rlog.i(LOG_TAG,"Phone State:" + mState);
  
          Rlog.i(LOG_TAG,"Ringing call: " + mRingingCall.toString());
  
          l = mRingingCall.getConnections();
          for (int i = 0, s = l.size(); i < s; i++) {
              Rlog.i(LOG_TAG,l.get(i).toString());
          }
  
          Rlog.i(LOG_TAG,"Foreground call: " + mForegroundCall.toString());
  
          l = mForegroundCall.getConnections();
          for (int i = 0, s = l.size(); i < s; i++) {
              Rlog.i(LOG_TAG,l.get(i).toString());
          }
  
          Rlog.i(LOG_TAG,"Background call: " + mBackgroundCall.toString());
  
          l = mBackgroundCall.getConnections();
          for (int i = 0, s = l.size(); i < s; i++) {
              Rlog.i(LOG_TAG,l.get(i).toString());
          }
  
      }
  
      //***** Called from GsmCdmaConnection
  
      public void hangup(GsmCdmaConnection conn) throws CallStateException {
          if (conn.mOwner != this) {
              throw new CallStateException ("GsmCdmaConnection " + conn
                                      + "does not belong to GsmCdmaCallTracker " + this);
          }
  
          if (conn == mPendingMO) {
              // We're hanging up an outgoing call that doesn't have it's
              // GsmCdma index assigned yet
  
              if (Phone.DEBUG_PHONE) log("hangup: set hangupPendingMO to true");
              mHangupPendingMO = true;
          } else if (!isPhoneTypeGsm()
                  && conn.getCall() == mRingingCall
                  && mRingingCall.getState() == GsmCdmaCall.State.WAITING) {
              // Handle call waiting hang up case.
              //
              // The ringingCall state will change to IDLE in GsmCdmaCall.detach
              // if the ringing call connection size is 0. We don't specifically
              // set the ringing call state to IDLE here to avoid a race condition
              // where a new call waiting could get a hang up from an old call
              // waiting ringingCall.
              //
              // PhoneApp does the call log itself since only PhoneApp knows
              // the hangup reason is user ignoring or timing out. So conn.onDisconnect()
              // is not called here. Instead, conn.onLocalDisconnect() is called.
              conn.onLocalDisconnect();
  
              updatePhoneState();
              mPhone.notifyPreciseCallStateChanged();
              return;
          } else {
              try {
                  mMetrics.writeRilHangup(mPhone.getPhoneId(), conn, conn.getGsmCdmaIndex(),
                          getNetworkCountryIso());
                  mCi.hangupConnection (conn.getGsmCdmaIndex(), obtainCompleteMessage());
              } catch (CallStateException ex) {
                  // Ignore "connection not found"
                  // Call may have hung up already
                  Rlog.w(LOG_TAG,"GsmCdmaCallTracker WARN: hangup() on absent connection "
                                  + conn);
              }
          }
  
          conn.onHangupLocal();
      }
  
      public void separate(GsmCdmaConnection conn) throws CallStateException {
          if (conn.mOwner != this) {
              throw new CallStateException ("GsmCdmaConnection " + conn
                                      + "does not belong to GsmCdmaCallTracker " + this);
          }
          try {
              mCi.separateConnection (conn.getGsmCdmaIndex(),
                  obtainCompleteMessage(EVENT_SEPARATE_RESULT));
          } catch (CallStateException ex) {
              // Ignore "connection not found"
              // Call may have hung up already
              Rlog.w(LOG_TAG,"GsmCdmaCallTracker WARN: separate() on absent connection " + conn);
          }
      }
  
      //***** Called from GsmCdmaPhone
  
      @UnsupportedAppUsage
      public void setMute(boolean mute) {
          mDesiredMute = mute;
          mCi.setMute(mDesiredMute, null);
      }
  
      public boolean getMute() {
          return mDesiredMute;
      }
  
  
      //***** Called from GsmCdmaCall
  
      public void hangup(GsmCdmaCall call) throws CallStateException {
          if (call.getConnections().size() == 0) {
              throw new CallStateException("no connections in call");
          }
  
          if (call == mRingingCall) {
              if (Phone.DEBUG_PHONE) log("(ringing) hangup waiting or background");
              logHangupEvent(call);
              mCi.hangupWaitingOrBackground(obtainCompleteMessage());
          } else if (call == mForegroundCall) {
              if (call.isDialingOrAlerting()) {
                  if (Phone.DEBUG_PHONE) {
                      log("(foregnd) hangup dialing or alerting...");
                  }
                  hangup((GsmCdmaConnection)(call.getConnections().get(0)));
              } else if (isPhoneTypeGsm()
                      && mRingingCall.isRinging()) {
                  // Do not auto-answer ringing on CHUP, instead just end active calls
                  log("hangup all conns in active/background call, without affecting ringing call");
                  hangupAllConnections(call);
              } else {
                  logHangupEvent(call);
                  hangupForegroundResumeBackground();
              }
          } else if (call == mBackgroundCall) {
              if (mRingingCall.isRinging()) {
                  if (Phone.DEBUG_PHONE) {
                      log("hangup all conns in background call");
                  }
                  hangupAllConnections(call);
              } else {
                  hangupWaitingOrBackground();
              }
          } else {
              throw new RuntimeException ("GsmCdmaCall " + call +
                      "does not belong to GsmCdmaCallTracker " + this);
          }
  
          call.onHangupLocal();
          mPhone.notifyPreciseCallStateChanged();
      }
  
      private void logHangupEvent(GsmCdmaCall call) {
          int count = call.mConnections.size();
          for (int i = 0; i < count; i++) {
              GsmCdmaConnection cn = (GsmCdmaConnection) call.mConnections.get(i);
              int call_index;
              try {
                  call_index = cn.getGsmCdmaIndex();
              } catch (CallStateException ex) {
                  call_index = -1;
              }
              mMetrics.writeRilHangup(mPhone.getPhoneId(), cn, call_index, getNetworkCountryIso());
          }
          if (VDBG) Rlog.v(LOG_TAG, "logHangupEvent logged " + count + " Connections ");
      }
  
      public void hangupWaitingOrBackground() {
          if (Phone.DEBUG_PHONE) log("hangupWaitingOrBackground");
          logHangupEvent(mBackgroundCall);
          mCi.hangupWaitingOrBackground(obtainCompleteMessage());
      }
  
      public void hangupForegroundResumeBackground() {
          if (Phone.DEBUG_PHONE) log("hangupForegroundResumeBackground");
          mCi.hangupForegroundResumeBackground(obtainCompleteMessage());
      }
  
      public void hangupConnectionByIndex(GsmCdmaCall call, int index)
              throws CallStateException {
          int count = call.mConnections.size();
          for (int i = 0; i < count; i++) {
              GsmCdmaConnection cn = (GsmCdmaConnection)call.mConnections.get(i);
              if (!cn.mDisconnected && cn.getGsmCdmaIndex() == index) {
                  mMetrics.writeRilHangup(mPhone.getPhoneId(), cn, cn.getGsmCdmaIndex(),
                          getNetworkCountryIso());
                  mCi.hangupConnection(index, obtainCompleteMessage());
                  return;
              }
          }
  
          throw new CallStateException("no GsmCdma index found");
      }
  
      public void hangupAllConnections(GsmCdmaCall call) {
          try {
              int count = call.mConnections.size();
              for (int i = 0; i < count; i++) {
                  GsmCdmaConnection cn = (GsmCdmaConnection)call.mConnections.get(i);
                  if (!cn.mDisconnected) {
                      mMetrics.writeRilHangup(mPhone.getPhoneId(), cn, cn.getGsmCdmaIndex(),
                              getNetworkCountryIso());
                      mCi.hangupConnection(cn.getGsmCdmaIndex(), obtainCompleteMessage());
                  }
              }
          } catch (CallStateException ex) {
              Rlog.e(LOG_TAG, "hangupConnectionByIndex caught " + ex);
          }
      }
  
      public GsmCdmaConnection getConnectionByIndex(GsmCdmaCall call, int index)
              throws CallStateException {
          int count = call.mConnections.size();
          for (int i = 0; i < count; i++) {
              GsmCdmaConnection cn = (GsmCdmaConnection)call.mConnections.get(i);
              if (!cn.mDisconnected && cn.getGsmCdmaIndex() == index) {
                  return cn;
              }
          }
  
          return null;
      }
  
      //CDMA
      private void notifyCallWaitingInfo(CdmaCallWaitingNotification obj) {
          if (mCallWaitingRegistrants != null) {
              mCallWaitingRegistrants.notifyRegistrants(new AsyncResult(null, obj, null));
          }
      }
  
      //CDMA
      private void handleCallWaitingInfo(CdmaCallWaitingNotification cw) {
          // Create a new GsmCdmaConnection which attaches itself to ringingCall.
          new GsmCdmaConnection(mPhone.getContext(), cw, this, mRingingCall);
          updatePhoneState();
  
          // Finally notify application
          notifyCallWaitingInfo(cw);
      }
  
      private Phone.SuppService getFailedService(int what) {
          switch (what) {
              case EVENT_SWITCH_RESULT:
                  return Phone.SuppService.SWITCH;
              case EVENT_CONFERENCE_RESULT:
                  return Phone.SuppService.CONFERENCE;
              case EVENT_SEPARATE_RESULT:
                  return Phone.SuppService.SEPARATE;
              case EVENT_ECT_RESULT:
                  return Phone.SuppService.TRANSFER;
          }
          return Phone.SuppService.UNKNOWN;
      }
  
      //****** Overridden from Handler
  
      @Override
      public void handleMessage(Message msg) {
          AsyncResult ar;
  
          switch (msg.what) {
              case EVENT_POLL_CALLS_RESULT:
                  Rlog.d(LOG_TAG, "Event EVENT_POLL_CALLS_RESULT Received");
  
                  if (msg == mLastRelevantPoll) {
                      if (DBG_POLL) log(
                              "handle EVENT_POLL_CALL_RESULT: set needsPoll=F");
                      mNeedsPoll = false;
                      mLastRelevantPoll = null;
                      handlePollCalls((AsyncResult)msg.obj);
                  }
              break;
  
              case EVENT_OPERATION_COMPLETE:
                  operationComplete();
              break;
  
              case EVENT_CONFERENCE_RESULT:
                  if (isPhoneTypeGsm()) {
                      ar = (AsyncResult) msg.obj;
                      if (ar.exception != null) {
                          // The conference merge failed, so notify listeners.  Ultimately this
                          // bubbles up to Telecom, which will inform the InCall UI of the failure.
                          Connection connection = mForegroundCall.getLatestConnection();
                          if (connection != null) {
                              connection.onConferenceMergeFailed();
                          }
                      }
                  }
                  // fall through
              case EVENT_SEPARATE_RESULT:
              case EVENT_ECT_RESULT:
              case EVENT_SWITCH_RESULT:
                  if (isPhoneTypeGsm()) {
                      ar = (AsyncResult) msg.obj;
                      if (ar.exception != null) {
                          mPhone.notifySuppServiceFailed(getFailedService(msg.what));
                      }
                      operationComplete();
                  } else {
                      if (msg.what != EVENT_SWITCH_RESULT) {
                          // EVENT_SWITCH_RESULT in GSM call triggers operationComplete() which gets
                          // the current call list. But in CDMA there is no list so there is nothing
                          // to do. Other messages however are not expected in CDMA.
                          throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
                                  "phone type " + mPhone.getPhoneType());
                      }
                  }
              break;
  
              case EVENT_GET_LAST_CALL_FAIL_CAUSE:
                  int causeCode;
                  String vendorCause = null;
                  ar = (AsyncResult)msg.obj;
  
                  operationComplete();
  
                  if (ar.exception != null) {
                      if (ar.exception instanceof CommandException) {
                          // If we get a CommandException, there are some modem-reported command
                          // errors which are truly exceptional.  We shouldn't treat these as
                          // NORMAL_CLEARING, so we'll re-map to ERROR_UNSPECIFIED.
                          CommandException commandException = (CommandException) ar.exception;
                          switch (commandException.getCommandError()) {
                              case RADIO_NOT_AVAILABLE:
                                  // Intentional fall-through.
                              case NO_MEMORY:
                                  // Intentional fall-through.
                              case INTERNAL_ERR:
                                  // Intentional fall-through.
                              case NO_RESOURCES:
                                  causeCode = CallFailCause.ERROR_UNSPECIFIED;
  
                                  // Report the actual internal command error as the vendor cause;
                                  // this will ensure it gets bubbled up into the Telecom logs.
                                  vendorCause = commandException.getCommandError().toString();
                                  break;
                              default:
                                  causeCode = CallFailCause.NORMAL_CLEARING;
                          }
                      } else {
                          // An exception occurred...just treat the disconnect
                          // cause as "normal"
                          causeCode = CallFailCause.NORMAL_CLEARING;
                          Rlog.i(LOG_TAG,
                                  "Exception during getLastCallFailCause, assuming normal "
                                          + "disconnect");
                      }
                  } else {
                      LastCallFailCause failCause = (LastCallFailCause)ar.result;
                      causeCode = failCause.causeCode;
                      vendorCause = failCause.vendorCause;
                  }
                  // Log the causeCode if its not normal
                  if (causeCode == CallFailCause.NO_CIRCUIT_AVAIL ||
                      causeCode == CallFailCause.TEMPORARY_FAILURE ||
                      causeCode == CallFailCause.SWITCHING_CONGESTION ||
                      causeCode == CallFailCause.CHANNEL_NOT_AVAIL ||
                      causeCode == CallFailCause.QOS_NOT_AVAIL ||
                      causeCode == CallFailCause.BEARER_NOT_AVAIL ||
                      causeCode == CallFailCause.ERROR_UNSPECIFIED) {
  
                      CellLocation loc = mPhone.getCellLocation();
                      int cid = -1;
                      if (loc != null) {
                          if (loc instanceof GsmCellLocation) {
                              cid = ((GsmCellLocation)loc).getCid();
                          } else if (loc instanceof CdmaCellLocation) {
                              cid = ((CdmaCellLocation)loc).getBaseStationId();
                          }
                      }
                      EventLog.writeEvent(EventLogTags.CALL_DROP, causeCode, cid,
                              TelephonyManager.getDefault().getNetworkType());
                  }
  
                  for (int i = 0, s = mDroppedDuringPoll.size(); i < s ; i++) {
                      GsmCdmaConnection conn = mDroppedDuringPoll.get(i);
  
                      conn.onRemoteDisconnect(causeCode, vendorCause);
                  }
  
                  updatePhoneState();
  
                  mPhone.notifyPreciseCallStateChanged();
                  mMetrics.writeRilCallList(mPhone.getPhoneId(), mDroppedDuringPoll,
                          getNetworkCountryIso());
                  mDroppedDuringPoll.clear();
              break;
  
              case EVENT_REPOLL_AFTER_DELAY:
              case EVENT_CALL_STATE_CHANGE:
                  pollCallsWhenSafe();
              break;
  
              case EVENT_RADIO_AVAILABLE:
                  handleRadioAvailable();
              break;
  
              case EVENT_RADIO_NOT_AVAILABLE:
                  handleRadioNotAvailable();
              break;
  
              case EVENT_EXIT_ECM_RESPONSE_CDMA:
                  if (!isPhoneTypeGsm()) {
                      // no matter the result, we still do the same here
                      if (mPendingCallInEcm) {
                          mCi.dial(mPendingMO.getAddress(), mPendingMO.isEmergencyCall(),
                                  mPendingMO.getEmergencyNumberInfo(),
                                  mPendingMO.hasKnownUserIntentEmergency(),
                                  mPendingCallClirMode, obtainCompleteMessage());
                          mPendingCallInEcm = false;
                      }
                      mPhone.unsetOnEcbModeExitResponse(this);
                  } else {
                      throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
                              "phone type " + mPhone.getPhoneType());
                  }
                  break;
  
              case EVENT_CALL_WAITING_INFO_CDMA:
                  if (!isPhoneTypeGsm()) {
                      ar = (AsyncResult)msg.obj;
                      if (ar.exception == null) {
                          handleCallWaitingInfo((CdmaCallWaitingNotification)ar.result);
                          Rlog.d(LOG_TAG, "Event EVENT_CALL_WAITING_INFO_CDMA Received");
                      }
                  } else {
                      throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
                              "phone type " + mPhone.getPhoneType());
                  }
                  break;
  
              case EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA:
                  if (!isPhoneTypeGsm()) {
                      ar = (AsyncResult)msg.obj;
                      if (ar.exception == null) {
                          // Assume 3 way call is connected
                          mPendingMO.onConnectedInOrOut();
                          mPendingMO = null;
                      }
                  } else {
                      throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
                              "phone type " + mPhone.getPhoneType());
                  }
                  break;
  
              case EVENT_THREE_WAY_DIAL_BLANK_FLASH:
                  if (!isPhoneTypeGsm()) {
                      ar = (AsyncResult) msg.obj;
                      if (ar.exception == null) {
                          postDelayed(
                                  new Runnable() {
                                      public void run() {
                                          if (mPendingMO != null) {
                                              mCi.sendCDMAFeatureCode(mPendingMO.getAddress(),
                                                      obtainMessage(EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA));
                                          }
                                      }
                                  }, m3WayCallFlashDelay);
                      } else {
                          mPendingMO = null;
                          Rlog.w(LOG_TAG, "exception happened on Blank Flash for 3-way call");
                      }
                  } else {
                      throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
                              "phone type " + mPhone.getPhoneType());
                  }
                  break;
  
              default:{
                  throw new RuntimeException("unexpected event " + msg.what + " not handled by " +
                          "phone type " + mPhone.getPhoneType());
              }
          }
      }
  
      /**
       * Dispatches the CS call radio technology to all exist connections.
       *
       * @param vrat the RIL voice radio technology for CS calls,
       *             see {@code RIL_RADIO_TECHNOLOGY_*} in {@link android.telephony.ServiceState}.
       */
      public void dispatchCsCallRadioTech(@ServiceState.RilRadioTechnology int vrat) {
          if (mConnections == null) {
              log("dispatchCsCallRadioTech: mConnections is null");
              return;
          }
          for (GsmCdmaConnection gsmCdmaConnection : mConnections) {
              if (gsmCdmaConnection != null) {
                  gsmCdmaConnection.setCallRadioTech(vrat);
              }
          }
      }
  
      //CDMA
      /**
       * Check and enable data call after an emergency call is dropped if it's
       * not in ECM
       */
      private void checkAndEnableDataCallAfterEmergencyCallDropped() {
          if (mIsInEmergencyCall) {
              mIsInEmergencyCall = false;
              boolean inEcm = mPhone.isInEcm();
              if (Phone.DEBUG_PHONE) {
                  log("checkAndEnableDataCallAfterEmergencyCallDropped,inEcm=" + inEcm);
              }
              if (!inEcm) {
                  // Re-initiate data connection
                  mPhone.getDataEnabledSettings().setInternalDataEnabled(true);
                  mPhone.notifyEmergencyCallRegistrants(false);
              }
              mPhone.sendEmergencyCallStateChange(false);
          }
      }
  
      /**
       * Check the MT call to see if it's a new ring or
       * a unknown connection.
       */
      private Connection checkMtFindNewRinging(DriverCall dc, int i) {
  
          Connection newRinging = null;
  
          // it's a ringing call
          if (mConnections[i].getCall() == mRingingCall) {
              newRinging = mConnections[i];
              if (Phone.DEBUG_PHONE) log("Notify new ring " + dc);
          } else {
              // Something strange happened: a call which is neither
              // a ringing call nor the one we created. It could be the
              // call collision result from RIL
              Rlog.e(LOG_TAG,"Phantom call appeared " + dc);
              // If it's a connected call, set the connect time so that
              // it's non-zero.  It may not be accurate, but at least
              // it won't appear as a Missed Call.
              if (dc.state != DriverCall.State.ALERTING
                      && dc.state != DriverCall.State.DIALING) {
                  mConnections[i].onConnectedInOrOut();
                  if (dc.state == DriverCall.State.HOLDING) {
                      // We've transitioned into HOLDING
                      mConnections[i].onStartedHolding();
                  }
              }
          }
          return newRinging;
      }
  
      //CDMA
      /**
       * Check if current call is in emergency call
       *
       * @return true if it is in emergency call
       *         false if it is not in emergency call
       */
      public boolean isInEmergencyCall() {
          return mIsInEmergencyCall;
      }
  
      /**
       * @return {@code true} if the pending outgoing call or active call is an OTASP call,
       * {@code false} otherwise.
       */
      public boolean isInOtaspCall() {
          return mPendingMO != null && mPendingMO.isOtaspCall()
                  || (mForegroundCall.getConnections().stream()
                  .filter(connection -> ((connection instanceof GsmCdmaConnection)
                          && (((GsmCdmaConnection) connection).isOtaspCall())))
                  .count() > 0);
      }
  
      @UnsupportedAppUsage
      private boolean isPhoneTypeGsm() {
          return mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM;
      }
  
      @UnsupportedAppUsage
      @Override
      public GsmCdmaPhone getPhone() {
          return mPhone;
      }
  
      @UnsupportedAppUsage
      @Override
      protected void log(String msg) {
          Rlog.d(LOG_TAG, "[" + mPhone.getPhoneId() + "] " + msg);
      }
  
      @Override
      public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
          pw.println("GsmCdmaCallTracker extends:");
          super.dump(fd, pw, args);
          pw.println("mConnections: length=" + mConnections.length);
          for(int i=0; i < mConnections.length; i++) {
              pw.printf("  mConnections[%d]=%s\n", i, mConnections[i]);
          }
          pw.println(" mVoiceCallEndedRegistrants=" + mVoiceCallEndedRegistrants);
          pw.println(" mVoiceCallStartedRegistrants=" + mVoiceCallStartedRegistrants);
          if (!isPhoneTypeGsm()) {
              pw.println(" mCallWaitingRegistrants=" + mCallWaitingRegistrants);
          }
          pw.println(" mDroppedDuringPoll: size=" + mDroppedDuringPoll.size());
          for(int i = 0; i < mDroppedDuringPoll.size(); i++) {
              pw.printf( "  mDroppedDuringPoll[%d]=%s\n", i, mDroppedDuringPoll.get(i));
          }
          pw.println(" mRingingCall=" + mRingingCall);
          pw.println(" mForegroundCall=" + mForegroundCall);
          pw.println(" mBackgroundCall=" + mBackgroundCall);
          pw.println(" mPendingMO=" + mPendingMO);
          pw.println(" mHangupPendingMO=" + mHangupPendingMO);
          pw.println(" mPhone=" + mPhone);
          pw.println(" mDesiredMute=" + mDesiredMute);
          pw.println(" mState=" + mState);
          if (!isPhoneTypeGsm()) {
              pw.println(" mPendingCallInEcm=" + mPendingCallInEcm);
              pw.println(" mIsInEmergencyCall=" + mIsInEmergencyCall);
              pw.println(" mPendingCallClirMode=" + mPendingCallClirMode);
              pw.println(" mIsEcmTimerCanceled=" + mIsEcmTimerCanceled);
          }
  
      }
  
      @Override
      public PhoneConstants.State getState() {
          return mState;
      }
  
      public int getMaxConnectionsPerCall() {
          return mPhone.isPhoneTypeGsm() ?
                  MAX_CONNECTIONS_PER_CALL_GSM :
                  MAX_CONNECTIONS_PER_CALL_CDMA;
      }
  
      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;
      }
  
      /**
       * Called to force the call tracker to cleanup any stale calls.  Does this by triggering
       * {@code GET_CURRENT_CALLS} on the RIL.
       */
      @Override
      public void cleanupCalls() {
          pollCallsWhenSafe();
      }
  }
  