content: /*
   * Copyright 2017 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 static android.provider.Telephony.CarrierId;
  
  import android.annotation.NonNull;
  import android.content.ContentValues;
  import android.content.Context;
  import android.content.Intent;
  import android.database.ContentObserver;
  import android.database.Cursor;
  import android.net.Uri;
  import android.os.Handler;
  import android.os.Message;
  import android.provider.Telephony;
  import android.service.carrier.CarrierIdentifier;
  import android.telephony.PhoneStateListener;
  import android.telephony.Rlog;
  import android.telephony.SubscriptionManager;
  import android.telephony.TelephonyManager;
  import android.text.TextUtils;
  import android.util.LocalLog;
  import android.util.Log;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.telephony.metrics.TelephonyMetrics;
  import com.android.internal.telephony.uicc.IccRecords;
  import com.android.internal.telephony.uicc.UiccController;
  import com.android.internal.util.IndentingPrintWriter;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  
  /**
   * CarrierResolver identifies the subscription carrier and returns a canonical carrier Id
   * and a user friendly carrier name. CarrierResolver reads subscription info and check against
   * all carrier matching rules stored in CarrierIdProvider. It is msim aware, each phone has a
   * dedicated CarrierResolver.
   */
  public class CarrierResolver extends Handler {
      private static final String LOG_TAG = CarrierResolver.class.getSimpleName();
      private static final boolean DBG = true;
      private static final boolean VDBG = Rlog.isLoggable(LOG_TAG, Log.VERBOSE);
  
      // events to trigger carrier identification
      private static final int SIM_LOAD_EVENT             = 1;
      private static final int ICC_CHANGED_EVENT          = 2;
      private static final int PREFER_APN_UPDATE_EVENT    = 3;
      private static final int CARRIER_ID_DB_UPDATE_EVENT = 4;
  
      private static final Uri CONTENT_URL_PREFER_APN = Uri.withAppendedPath(
              Telephony.Carriers.CONTENT_URI, "preferapn");
  
      // cached matching rules based mccmnc to speed up resolution
      private List<CarrierMatchingRule> mCarrierMatchingRulesOnMccMnc = new ArrayList<>();
      // cached carrier Id
      private int mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
      // cached specific carrier Id
      private int mSpecificCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
      // cached MNO carrier Id. mno carrier shares the same mccmnc as cid and can be solely
      // identified by mccmnc only. If there is no such mno carrier, mno carrier id equals to
      // the cid.
      private int mMnoCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
      // cached carrier name
      private String mCarrierName;
      private String mSpecificCarrierName;
      // cached preferapn name
      private String mPreferApn;
      // override for testing purpose
      private String mTestOverrideApn;
      private String mTestOverrideCarrierPriviledgeRule;
      // cached service provider name. telephonyManager API returns empty string as default value.
      // some carriers need to target devices with Empty SPN. In that case, carrier matching rule
      // should specify "" spn explicitly.
      private String mSpn = "";
  
      private Context mContext;
      private Phone mPhone;
      private IccRecords mIccRecords;
      private final LocalLog mCarrierIdLocalLog = new LocalLog(20);
      private final TelephonyManager mTelephonyMgr;
  
      private final ContentObserver mContentObserver = new ContentObserver(this) {
          @Override
          public void onChange(boolean selfChange, Uri uri) {
              if (CONTENT_URL_PREFER_APN.equals(uri.getLastPathSegment())) {
                  logd("onChange URI: " + uri);
                  sendEmptyMessage(PREFER_APN_UPDATE_EVENT);
              } else if (CarrierId.All.CONTENT_URI.equals(uri)) {
                  logd("onChange URI: " + uri);
                  sendEmptyMessage(CARRIER_ID_DB_UPDATE_EVENT);
              }
          }
      };
  
      public CarrierResolver(Phone phone) {
          logd("Creating CarrierResolver[" + phone.getPhoneId() + "]");
          mContext = phone.getContext();
          mPhone = phone;
          mTelephonyMgr = TelephonyManager.from(mContext);
  
          // register events
          mContext.getContentResolver().registerContentObserver(CONTENT_URL_PREFER_APN, false,
                  mContentObserver);
          mContext.getContentResolver().registerContentObserver(
                  CarrierId.All.CONTENT_URI, false, mContentObserver);
          UiccController.getInstance().registerForIccChanged(this, ICC_CHANGED_EVENT, null);
      }
  
      /**
       * This is triggered from SubscriptionInfoUpdater after sim state change.
       * The sequence of sim loading would be
       *  1. ACTION_SUBINFO_CONTENT_CHANGE
       *  2. ACTION_SIM_STATE_CHANGED/ACTION_SIM_CARD_STATE_CHANGED
       *  /ACTION_SIM_APPLICATION_STATE_CHANGED
       *  3. ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED
       *
       *  For SIM refresh either reset or init refresh type, SubscriptionInfoUpdater will re-trigger
       *  carrier identification with sim loaded state. Framework today silently handle single file
       *  refresh type.
       *  TODO: check fileId from single file refresh, if the refresh file is IMSI, gid1 or other
       *  records which might change carrier id, framework should trigger sim loaded state just like
       *  other refresh events: INIT or RESET and which will ultimately trigger carrier
       *  re-identification.
       */
      public void resolveSubscriptionCarrierId(String simState) {
          logd("[resolveSubscriptionCarrierId] simState: " + simState);
          switch (simState) {
              case IccCardConstants.INTENT_VALUE_ICC_ABSENT:
              case IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR:
                  // only clear carrier id on absent to avoid transition to unknown carrier id during
                  // intermediate states of sim refresh
                  handleSimAbsent();
                  break;
              case IccCardConstants.INTENT_VALUE_ICC_LOADED:
                  handleSimLoaded();
                  break;
          }
      }
  
      private void handleSimLoaded() {
          if (mIccRecords != null) {
              /**
               * returns empty string to be consistent with
               * {@link TelephonyManager#getSimOperatorName()}
               */
              mSpn = (mIccRecords.getServiceProviderName() == null) ? ""
                      : mIccRecords.getServiceProviderName();
          } else {
              loge("mIccRecords is null on SIM_LOAD_EVENT, could not get SPN");
          }
          mPreferApn = getPreferApn();
          loadCarrierMatchingRulesOnMccMnc();
      }
  
      private void handleSimAbsent() {
          mCarrierMatchingRulesOnMccMnc.clear();
          mSpn = null;
          mPreferApn = null;
          updateCarrierIdAndName(TelephonyManager.UNKNOWN_CARRIER_ID, null,
                  TelephonyManager.UNKNOWN_CARRIER_ID, null,
                  TelephonyManager.UNKNOWN_CARRIER_ID);
      }
  
      private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
          @Override
          public void onCallStateChanged(int state, String ignored) {
          }
      };
  
      /**
       * Entry point for the carrier identification.
       *
       *    1. SIM_LOAD_EVENT
       *        This indicates that all SIM records has been loaded and its first entry point for the
       *        carrier identification. Note, there are other attributes could be changed on the fly
       *        like APN. We cached all carrier matching rules based on MCCMNC to speed
       *        up carrier resolution on following trigger events.
       *
       *    2. PREFER_APN_UPDATE_EVENT
       *        This indicates prefer apn has been changed. It could be triggered when user modified
       *        APN settings or when default data connection first establishes on the current carrier.
       *        We follow up on this by querying prefer apn sqlite and re-issue carrier identification
       *        with the updated prefer apn name.
       *
       *    3. CARRIER_ID_DB_UPDATE_EVENT
       *        This indicates that carrierIdentification database which stores all matching rules
       *        has been updated. It could be triggered from OTA or assets update.
       */
      @Override
      public void handleMessage(Message msg) {
          if (DBG) logd("handleMessage: " + msg.what);
          switch (msg.what) {
              case SIM_LOAD_EVENT:
                  handleSimLoaded();
                  break;
              case CARRIER_ID_DB_UPDATE_EVENT:
                  loadCarrierMatchingRulesOnMccMnc();
                  break;
              case PREFER_APN_UPDATE_EVENT:
                  String preferApn = getPreferApn();
                  if (!equals(mPreferApn, preferApn, true)) {
                      logd("[updatePreferApn] from:" + mPreferApn + " to:" + preferApn);
                      mPreferApn = preferApn;
                      matchSubscriptionCarrier();
                  }
                  break;
              case ICC_CHANGED_EVENT:
                  // all records used for carrier identification are from SimRecord.
                  final IccRecords newIccRecords = UiccController.getInstance().getIccRecords(
                          mPhone.getPhoneId(), UiccController.APP_FAM_3GPP);
                  if (mIccRecords != newIccRecords) {
                      if (mIccRecords != null) {
                          logd("Removing stale icc objects.");
                          mIccRecords.unregisterForRecordsOverride(this);
                          mIccRecords = null;
                      }
                      if (newIccRecords != null) {
                          logd("new Icc object");
                          newIccRecords.registerForRecordsOverride(this, SIM_LOAD_EVENT, null);
                          mIccRecords = newIccRecords;
                      }
                  }
                  break;
              default:
                  loge("invalid msg: " + msg.what);
                  break;
          }
      }
  
      private void loadCarrierMatchingRulesOnMccMnc() {
          try {
              String mccmnc = mTelephonyMgr.getSimOperatorNumericForPhone(mPhone.getPhoneId());
              Cursor cursor = mContext.getContentResolver().query(
                      CarrierId.All.CONTENT_URI,
                      /* projection */ null,
                      /* selection */ CarrierId.All.MCCMNC + "=?",
                      /* selectionArgs */ new String[]{mccmnc}, null);
              try {
                  if (cursor != null) {
                      if (VDBG) {
                          logd("[loadCarrierMatchingRules]- " + cursor.getCount()
                                  + " Records(s) in DB" + " mccmnc: " + mccmnc);
                      }
                      mCarrierMatchingRulesOnMccMnc.clear();
                      while (cursor.moveToNext()) {
                          mCarrierMatchingRulesOnMccMnc.add(makeCarrierMatchingRule(cursor));
                      }
                      matchSubscriptionCarrier();
                  }
              } finally {
                  if (cursor != null) {
                      cursor.close();
                  }
              }
          } catch (Exception ex) {
              loge("[loadCarrierMatchingRules]- ex: " + ex);
          }
      }
  
      private String getCarrierNameFromId(int cid) {
          try {
              Cursor cursor = mContext.getContentResolver().query(
                      CarrierId.All.CONTENT_URI,
                      /* projection */ null,
                      /* selection */ CarrierId.CARRIER_ID + "=?",
                      /* selectionArgs */ new String[]{cid + ""}, null);
              try {
                  if (cursor != null) {
                      if (VDBG) {
                          logd("[getCarrierNameFromId]- " + cursor.getCount()
                                  + " Records(s) in DB" + " cid: " + cid);
                      }
                      while (cursor.moveToNext()) {
                          return cursor.getString(cursor.getColumnIndex(CarrierId.CARRIER_NAME));
                      }
                  }
              } finally {
                  if (cursor != null) {
                      cursor.close();
                  }
              }
          } catch (Exception ex) {
              loge("[getCarrierNameFromId]- ex: " + ex);
          }
          return null;
      }
  
      private static List<CarrierMatchingRule> getCarrierMatchingRulesFromMccMnc(
              @NonNull Context context, String mccmnc) {
          List<CarrierMatchingRule> rules = new ArrayList<>();
          try {
              Cursor cursor = context.getContentResolver().query(
                      CarrierId.All.CONTENT_URI,
                      /* projection */ null,
                      /* selection */ CarrierId.All.MCCMNC + "=?",
                      /* selectionArgs */ new String[]{mccmnc}, null);
              try {
                  if (cursor != null) {
                      if (VDBG) {
                          logd("[loadCarrierMatchingRules]- " + cursor.getCount()
                                  + " Records(s) in DB" + " mccmnc: " + mccmnc);
                      }
                      rules.clear();
                      while (cursor.moveToNext()) {
                          rules.add(makeCarrierMatchingRule(cursor));
                      }
                  }
              } finally {
                  if (cursor != null) {
                      cursor.close();
                  }
              }
          } catch (Exception ex) {
              loge("[loadCarrierMatchingRules]- ex: " + ex);
          }
          return rules;
      }
  
      private String getPreferApn() {
          // return test overrides if present
          if (!TextUtils.isEmpty(mTestOverrideApn)) {
              logd("[getPreferApn]- " + mTestOverrideApn + " test override");
              return mTestOverrideApn;
          }
          Cursor cursor = mContext.getContentResolver().query(
                  Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "preferapn/subId/"
                  + mPhone.getSubId()), /* projection */ new String[]{Telephony.Carriers.APN},
                  /* selection */ null, /* selectionArgs */ null, /* sortOrder */ null);
          try {
              if (cursor != null) {
                  if (VDBG) {
                      logd("[getPreferApn]- " + cursor.getCount() + " Records(s) in DB");
                  }
                  while (cursor.moveToNext()) {
                      String apn = cursor.getString(cursor.getColumnIndexOrThrow(
                              Telephony.Carriers.APN));
                      logd("[getPreferApn]- " + apn);
                      return apn;
                  }
              }
          } catch (Exception ex) {
              loge("[getPreferApn]- exception: " + ex);
          } finally {
              if (cursor != null) {
                  cursor.close();
              }
          }
          return null;
      }
  
      private boolean isPreferApnUserEdited(@NonNull String preferApn) {
          try (Cursor cursor = mContext.getContentResolver().query(
                  Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI,
                          "preferapn/subId/" + mPhone.getSubId()),
                  /* projection */ new String[]{Telephony.Carriers.EDITED_STATUS},
                  /* selection */ Telephony.Carriers.APN + "=?",
                  /* selectionArgs */ new String[]{preferApn}, /* sortOrder */ null) ) {
              if (cursor != null && cursor.moveToFirst()) {
                  return cursor.getInt(cursor.getColumnIndexOrThrow(
                          Telephony.Carriers.EDITED_STATUS)) == Telephony.Carriers.USER_EDITED;
              }
          } catch (Exception ex) {
              loge("[isPreferApnUserEdited]- exception: " + ex);
          }
          return false;
      }
  
      public void setTestOverrideApn(String apn) {
          logd("[setTestOverrideApn]: " + apn);
          mTestOverrideApn = apn;
      }
  
      public void setTestOverrideCarrierPriviledgeRule(String rule) {
          logd("[setTestOverrideCarrierPriviledgeRule]: " + rule);
          mTestOverrideCarrierPriviledgeRule = rule;
      }
  
      private void updateCarrierIdAndName(int cid, String name,
                                          int specificCarrierId, String specificCarrierName,
                                          int mnoCid) {
          boolean update = false;
          if (specificCarrierId != mSpecificCarrierId) {
              logd("[updateSpecificCarrierId] from:" + mSpecificCarrierId + " to:"
                      + specificCarrierId);
              mSpecificCarrierId = specificCarrierId;
              update = true;
          }
          if (specificCarrierName != mSpecificCarrierName) {
              logd("[updateSpecificCarrierName] from:" + mSpecificCarrierName + " to:"
                      + specificCarrierName);
              mSpecificCarrierName = specificCarrierName;
              update = true;
          }
          if (update) {
              mCarrierIdLocalLog.log("[updateSpecificCarrierIdAndName] cid:"
                      + mSpecificCarrierId + " name:" + mSpecificCarrierName);
              final Intent intent = new Intent(TelephonyManager
                      .ACTION_SUBSCRIPTION_SPECIFIC_CARRIER_IDENTITY_CHANGED);
              intent.putExtra(TelephonyManager.EXTRA_SPECIFIC_CARRIER_ID, mSpecificCarrierId);
              intent.putExtra(TelephonyManager.EXTRA_SPECIFIC_CARRIER_NAME, mSpecificCarrierName);
              intent.putExtra(TelephonyManager.EXTRA_SUBSCRIPTION_ID, mPhone.getSubId());
              mContext.sendBroadcast(intent);
  
              // notify content observers for specific carrier id change event.
              ContentValues cv = new ContentValues();
              cv.put(CarrierId.SPECIFIC_CARRIER_ID, mSpecificCarrierId);
              cv.put(CarrierId.SPECIFIC_CARRIER_ID_NAME, mSpecificCarrierName);
              mContext.getContentResolver().update(
                      Telephony.CarrierId.getSpecificCarrierIdUriForSubscriptionId(mPhone.getSubId()),
                      cv, null, null);
          }
  
          update = false;
          if (!equals(name, mCarrierName, true)) {
              logd("[updateCarrierName] from:" + mCarrierName + " to:" + name);
              mCarrierName = name;
              update = true;
          }
          if (cid != mCarrierId) {
              logd("[updateCarrierId] from:" + mCarrierId + " to:" + cid);
              mCarrierId = cid;
              update = true;
          }
          if (mnoCid != mMnoCarrierId) {
              logd("[updateMnoCarrierId] from:" + mMnoCarrierId + " to:" + mnoCid);
              mMnoCarrierId = mnoCid;
              update = true;
          }
          if (update) {
              mCarrierIdLocalLog.log("[updateCarrierIdAndName] cid:" + mCarrierId + " name:"
                      + mCarrierName + " mnoCid:" + mMnoCarrierId);
              final Intent intent = new Intent(TelephonyManager
                      .ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED);
              intent.putExtra(TelephonyManager.EXTRA_CARRIER_ID, mCarrierId);
              intent.putExtra(TelephonyManager.EXTRA_CARRIER_NAME, mCarrierName);
              intent.putExtra(TelephonyManager.EXTRA_SUBSCRIPTION_ID, mPhone.getSubId());
              mContext.sendBroadcast(intent);
  
              // notify content observers for carrier id change event
              ContentValues cv = new ContentValues();
              cv.put(CarrierId.CARRIER_ID, mCarrierId);
              cv.put(CarrierId.CARRIER_NAME, mCarrierName);
              mContext.getContentResolver().update(
                      Telephony.CarrierId.getUriForSubscriptionId(mPhone.getSubId()), cv, null, null);
          }
          // during esim profile switch, there is no sim absent thus carrier id will persist and
          // might not trigger an update if switch profiles for the same carrier. thus always update
          // subscriptioninfo db to make sure we have correct carrier id set.
          if (SubscriptionManager.isValidSubscriptionId(mPhone.getSubId())) {
              // only persist carrier id to simInfo db when subId is valid.
              SubscriptionController.getInstance().setCarrierId(mCarrierId, mPhone.getSubId());
          }
      }
  
      private static CarrierMatchingRule makeCarrierMatchingRule(Cursor cursor) {
          String certs = cursor.getString(
                  cursor.getColumnIndexOrThrow(CarrierId.All.PRIVILEGE_ACCESS_RULE));
          return new CarrierMatchingRule(
                  cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.MCCMNC)),
                  cursor.getString(cursor.getColumnIndexOrThrow(
                          CarrierId.All.IMSI_PREFIX_XPATTERN)),
                  cursor.getString(cursor.getColumnIndexOrThrow(
                          CarrierId.All.ICCID_PREFIX)),
                  cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.GID1)),
                  cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.GID2)),
                  cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.PLMN)),
                  cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.SPN)),
                  cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.APN)),
                  (TextUtils.isEmpty(certs) ? null : new ArrayList<>(Arrays.asList(certs))),
                  cursor.getInt(cursor.getColumnIndexOrThrow(CarrierId.CARRIER_ID)),
                  cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.CARRIER_NAME)),
                  cursor.getInt(cursor.getColumnIndexOrThrow(CarrierId.PARENT_CARRIER_ID)));
      }
  
      /**
       * carrier matching attributes with corresponding cid
       */
      public static class CarrierMatchingRule {
          /**
           * These scores provide the hierarchical relationship between the attributes, intended to
           * resolve conflicts in a deterministic way. The scores are constructed such that a match
           * from a higher tier will beat any subsequent match which does not match at that tier,
           * so MCCMNC beats everything else. This avoids problems when two (or more) carriers rule
           * matches as the score helps to find the best match uniquely. e.g.,
           * rule 1 {mccmnc, imsi} rule 2 {mccmnc, imsi, gid1} and rule 3 {mccmnc, imsi, gid2} all
           * matches with subscription data. rule 2 wins with the highest matching score.
           */
          private static final int SCORE_MCCMNC                   = 1 << 8;
          private static final int SCORE_IMSI_PREFIX              = 1 << 7;
          private static final int SCORE_ICCID_PREFIX             = 1 << 6;
          private static final int SCORE_GID1                     = 1 << 5;
          private static final int SCORE_GID2                     = 1 << 4;
          private static final int SCORE_PLMN                     = 1 << 3;
          private static final int SCORE_PRIVILEGE_ACCESS_RULE    = 1 << 2;
          private static final int SCORE_SPN                      = 1 << 1;
          private static final int SCORE_APN                      = 1 << 0;
  
          private static final int SCORE_INVALID                  = -1;
  
          // carrier matching attributes
          public final String mccMnc;
          public final String imsiPrefixPattern;
          public final String iccidPrefix;
          public final String gid1;
          public final String gid2;
          public final String plmn;
          public final String spn;
          public final String apn;
          // there can be multiple certs configured in the UICC
          public final List<String> privilegeAccessRule;
  
          // user-facing carrier name
          private String mName;
          // unique carrier id
          private int mCid;
          // unique parent carrier id
          private int mParentCid;
  
          private int mScore = 0;
  
          @VisibleForTesting
          public CarrierMatchingRule(String mccmnc, String imsiPrefixPattern, String iccidPrefix,
                  String gid1, String gid2, String plmn, String spn, String apn,
                  List<String> privilegeAccessRule, int cid, String name, int parentCid) {
              mccMnc = mccmnc;
              this.imsiPrefixPattern = imsiPrefixPattern;
              this.iccidPrefix = iccidPrefix;
              this.gid1 = gid1;
              this.gid2 = gid2;
              this.plmn = plmn;
              this.spn = spn;
              this.apn = apn;
              this.privilegeAccessRule = privilegeAccessRule;
              mCid = cid;
              mName = name;
              mParentCid = parentCid;
          }
  
          private CarrierMatchingRule(CarrierMatchingRule rule) {
              mccMnc = rule.mccMnc;
              imsiPrefixPattern = rule.imsiPrefixPattern;
              iccidPrefix = rule.iccidPrefix;
              gid1 = rule.gid1;
              gid2 = rule.gid2;
              plmn = rule.plmn;
              spn = rule.spn;
              apn = rule.apn;
              privilegeAccessRule = rule.privilegeAccessRule;
              mCid = rule.mCid;
              mName = rule.mName;
              mParentCid = rule.mParentCid;
          }
  
          // Calculate matching score. Values which aren't set in the rule are considered "wild".
          // All values in the rule must match in order for the subscription to be considered part of
          // the carrier. Otherwise, a invalid score -1 will be assigned. A match from a higher tier
          // will beat any subsequent match which does not match at that tier. When there are multiple
          // matches at the same tier, the match with highest score will be used.
          public void match(CarrierMatchingRule subscriptionRule) {
              mScore = 0;
              if (mccMnc != null) {
                  if (!CarrierResolver.equals(subscriptionRule.mccMnc, mccMnc, false)) {
                      mScore = SCORE_INVALID;
                      return;
                  }
                  mScore += SCORE_MCCMNC;
              }
              if (imsiPrefixPattern != null) {
                  if (!imsiPrefixMatch(subscriptionRule.imsiPrefixPattern, imsiPrefixPattern)) {
                      mScore = SCORE_INVALID;
                      return;
                  }
                  mScore += SCORE_IMSI_PREFIX;
              }
              if (iccidPrefix != null) {
                  if (!iccidPrefixMatch(subscriptionRule.iccidPrefix, iccidPrefix)) {
                      mScore = SCORE_INVALID;
                      return;
                  }
                  mScore += SCORE_ICCID_PREFIX;
              }
              if (gid1 != null) {
                  if (!gidMatch(subscriptionRule.gid1, gid1)) {
                      mScore = SCORE_INVALID;
                      return;
                  }
                  mScore += SCORE_GID1;
              }
              if (gid2 != null) {
                  if (!gidMatch(subscriptionRule.gid2, gid2)) {
                      mScore = SCORE_INVALID;
                      return;
                  }
                  mScore += SCORE_GID2;
              }
              if (plmn != null) {
                  if (!CarrierResolver.equals(subscriptionRule.plmn, plmn, true)) {
                      mScore = SCORE_INVALID;
                      return;
                  }
                  mScore += SCORE_PLMN;
              }
              if (spn != null) {
                  if (!CarrierResolver.equals(subscriptionRule.spn, spn, true)) {
                      mScore = SCORE_INVALID;
                      return;
                  }
                  mScore += SCORE_SPN;
              }
  
              if (privilegeAccessRule != null && !privilegeAccessRule.isEmpty()) {
                  if (!carrierPrivilegeRulesMatch(subscriptionRule.privilegeAccessRule,
                          privilegeAccessRule)) {
                      mScore = SCORE_INVALID;
                      return;
                  }
                  mScore += SCORE_PRIVILEGE_ACCESS_RULE;
              }
  
              if (apn != null) {
                  if (!CarrierResolver.equals(subscriptionRule.apn, apn, true)) {
                      mScore = SCORE_INVALID;
                      return;
                  }
                  mScore += SCORE_APN;
              }
          }
  
          private boolean imsiPrefixMatch(String imsi, String prefixXPattern) {
              if (TextUtils.isEmpty(prefixXPattern)) return true;
              if (TextUtils.isEmpty(imsi)) return false;
              if (imsi.length() < prefixXPattern.length()) {
                  return false;
              }
              for (int i = 0; i < prefixXPattern.length(); i++) {
                  if ((prefixXPattern.charAt(i) != 'x') && (prefixXPattern.charAt(i) != 'X')
                          && (prefixXPattern.charAt(i) != imsi.charAt(i))) {
                      return false;
                  }
              }
              return true;
          }
  
          private boolean iccidPrefixMatch(String iccid, String prefix) {
              if (iccid == null || prefix == null) {
                  return false;
              }
              return iccid.startsWith(prefix);
          }
  
          // We are doing prefix and case insensitive match.
          // Ideally we should do full string match. However due to SIM manufacture issues
          // gid from some SIM might has garbage tail.
          private boolean gidMatch(String gidFromSim, String gid) {
              return (gidFromSim != null) && gidFromSim.toLowerCase().startsWith(gid.toLowerCase());
          }
  
          private boolean carrierPrivilegeRulesMatch(List<String> certsFromSubscription,
                                                     List<String> certs) {
              if (certsFromSubscription == null || certsFromSubscription.isEmpty()) {
                  return false;
              }
              for (String cert : certs) {
                  for (String certFromSubscription : certsFromSubscription) {
                      if (!TextUtils.isEmpty(cert)
                              && cert.equalsIgnoreCase(certFromSubscription)) {
                          return true;
                      }
                  }
              }
              return false;
          }
  
          public String toString() {
              return "[CarrierMatchingRule] -"
                      + " mccmnc: " + mccMnc
                      + " gid1: " + gid1
                      + " gid2: " + gid2
                      + " plmn: " + plmn
                      + " imsi_prefix: " + imsiPrefixPattern
                      + " iccid_prefix" + iccidPrefix
                      + " spn: " + spn
                      + " privilege_access_rule: " + privilegeAccessRule
                      + " apn: " + apn
                      + " name: " + mName
                      + " cid: " + mCid
                      + " score: " + mScore;
          }
      }
  
      private CarrierMatchingRule getSubscriptionMatchingRule() {
          final String mccmnc = mTelephonyMgr.getSimOperatorNumericForPhone(mPhone.getPhoneId());
          final String iccid = mPhone.getIccSerialNumber();
          final String gid1 = mPhone.getGroupIdLevel1();
          final String gid2 = mPhone.getGroupIdLevel2();
          final String imsi = mPhone.getSubscriberId();
          final String plmn = mPhone.getPlmn();
          final String spn = mSpn;
          final String apn = mPreferApn;
          List<String> accessRules;
          // check if test override present
          if (!TextUtils.isEmpty(mTestOverrideCarrierPriviledgeRule)) {
              accessRules = new ArrayList<>(Arrays.asList(mTestOverrideCarrierPriviledgeRule));
          } else {
              accessRules = mTelephonyMgr.createForSubscriptionId(mPhone.getSubId())
                      .getCertsFromCarrierPrivilegeAccessRules();
          }
  
          if (VDBG) {
              logd("[matchSubscriptionCarrier]"
                      + " mnnmnc:" + mccmnc
                      + " gid1: " + gid1
                      + " gid2: " + gid2
                      + " imsi: " + Rlog.pii(LOG_TAG, imsi)
                      + " iccid: " + Rlog.pii(LOG_TAG, iccid)
                      + " plmn: " + plmn
                      + " spn: " + spn
                      + " apn: " + apn
                      + " accessRules: " + ((accessRules != null) ? accessRules : null));
          }
          return new CarrierMatchingRule(
                  mccmnc, imsi, iccid, gid1, gid2, plmn, spn, apn, accessRules,
                  TelephonyManager.UNKNOWN_CARRIER_ID, null,
                  TelephonyManager.UNKNOWN_CARRIER_ID);
      }
  
      /**
       * find the best matching carrier from candidates with matched subscription MCCMNC.
       */
      private void matchSubscriptionCarrier() {
          if (!SubscriptionManager.isValidSubscriptionId(mPhone.getSubId())) {
              logd("[matchSubscriptionCarrier]" + "skip before sim records loaded");
              return;
          }
          int maxScore = CarrierMatchingRule.SCORE_INVALID;
          /**
           * For child-parent relationship. either child and parent have the same matching
           * score, or child's matching score > parents' matching score.
           */
          CarrierMatchingRule maxRule = null;
          CarrierMatchingRule maxRuleParent = null;
          /**
           * matching rule with mccmnc only. If mnoRule is found, then mno carrier id equals to the
           * cid from mnoRule. otherwise, mno carrier id is same as cid.
           */
          CarrierMatchingRule mnoRule = null;
          CarrierMatchingRule subscriptionRule = getSubscriptionMatchingRule();
  
          for (CarrierMatchingRule rule : mCarrierMatchingRulesOnMccMnc) {
              rule.match(subscriptionRule);
              if (rule.mScore > maxScore) {
                  maxScore = rule.mScore;
                  maxRule = rule;
                  maxRuleParent = rule;
              } else if (maxScore > CarrierMatchingRule.SCORE_INVALID && rule.mScore == maxScore) {
                  // to handle the case that child parent has the same matching score, we need to
                  // differentiate who is child who is parent.
                  if (rule.mParentCid == maxRule.mCid) {
                      maxRule = rule;
                  } else if (maxRule.mParentCid == rule.mCid) {
                      maxRuleParent = rule;
                  }
              }
              if (rule.mScore == CarrierMatchingRule.SCORE_MCCMNC) {
                  mnoRule = rule;
              }
          }
          if (maxScore == CarrierMatchingRule.SCORE_INVALID) {
              logd("[matchSubscriptionCarrier - no match] cid: " + TelephonyManager.UNKNOWN_CARRIER_ID
                      + " name: " + null);
              updateCarrierIdAndName(TelephonyManager.UNKNOWN_CARRIER_ID, null,
                      TelephonyManager.UNKNOWN_CARRIER_ID, null,
                      TelephonyManager.UNKNOWN_CARRIER_ID);
          } else {
              // if there is a single matching result, check if this rule has parent cid assigned.
              if ((maxRule == maxRuleParent)
                      && maxRule.mParentCid != TelephonyManager.UNKNOWN_CARRIER_ID) {
                  maxRuleParent = new CarrierMatchingRule(maxRule);
                  maxRuleParent.mCid = maxRuleParent.mParentCid;
                  maxRuleParent.mName = getCarrierNameFromId(maxRuleParent.mCid);
              }
              logd("[matchSubscriptionCarrier] specific cid: " + maxRule.mCid
                      + " specific name: " + maxRule.mName +" cid: " + maxRuleParent.mCid
                      + " name: " + maxRuleParent.mName);
              updateCarrierIdAndName(maxRuleParent.mCid, maxRuleParent.mName,
                      maxRule.mCid, maxRule.mName,
                      (mnoRule == null) ? maxRule.mCid : mnoRule.mCid);
          }
  
          /*
           * Write Carrier Identification Matching event, logging with the
           * carrierId, mccmnc, gid1 and carrier list version to differentiate below cases of metrics:
           * 1) unknown mccmnc - the Carrier Id provider contains no rule that matches the
           * read mccmnc.
           * 2) the Carrier Id provider contains some rule(s) that match the read mccmnc,
           * but the read gid1 is not matched within the highest-scored rule.
           * 3) successfully found a matched carrier id in the provider.
           * 4) use carrier list version to compare the unknown carrier ratio between each version.
           */
          String unknownGid1ToLog = ((maxScore & CarrierMatchingRule.SCORE_GID1) == 0
                  && !TextUtils.isEmpty(subscriptionRule.gid1)) ? subscriptionRule.gid1 : null;
          String unknownMccmncToLog = ((maxScore == CarrierMatchingRule.SCORE_INVALID
                  || (maxScore & CarrierMatchingRule.SCORE_GID1) == 0)
                  && !TextUtils.isEmpty(subscriptionRule.mccMnc)) ? subscriptionRule.mccMnc : null;
  
          // pass subscription rule to metrics. scrub all possible PII before uploading.
          // only log apn if not user edited.
          String apn = (subscriptionRule.apn != null
                  && !isPreferApnUserEdited(subscriptionRule.apn))
                  ? subscriptionRule.apn : null;
          // only log first 7 bits of iccid
          String iccidPrefix = (subscriptionRule.iccidPrefix != null)
                  && (subscriptionRule.iccidPrefix.length() >= 7)
                  ? subscriptionRule.iccidPrefix.substring(0, 7) : subscriptionRule.iccidPrefix;
          // only log first 8 bits of imsi
          String imsiPrefix = (subscriptionRule.imsiPrefixPattern != null)
                  && (subscriptionRule.imsiPrefixPattern.length() >= 8)
                  ? subscriptionRule.imsiPrefixPattern.substring(0, 8)
                  : subscriptionRule.imsiPrefixPattern;
  
          CarrierMatchingRule simInfo = new CarrierMatchingRule(
                  subscriptionRule.mccMnc,
                  imsiPrefix,
                  iccidPrefix,
                  subscriptionRule.gid1,
                  subscriptionRule.gid2,
                  subscriptionRule.plmn,
                  subscriptionRule.spn,
                  apn,
                  subscriptionRule.privilegeAccessRule,
                  -1, null, -1);
  
          TelephonyMetrics.getInstance().writeCarrierIdMatchingEvent(
                  mPhone.getPhoneId(), getCarrierListVersion(), mCarrierId,
                  unknownMccmncToLog, unknownGid1ToLog, simInfo);
      }
  
      public int getCarrierListVersion() {
          final Cursor cursor = mContext.getContentResolver().query(
                  Uri.withAppendedPath(CarrierId.All.CONTENT_URI,
                  "get_version"), null, null, null);
          cursor.moveToFirst();
          return cursor.getInt(0);
      }
  
      public int getCarrierId() {
          return mCarrierId;
      }
      /**
       * Returns fine-grained carrier id of the current subscription. Carrier ids with a valid parent
       * id are specific carrier ids.
       *
       * A specific carrier ID can represent the fact that a carrier may be in effect an aggregation
       * of other carriers (ie in an MVNO type scenario) where each of these specific carriers which
       * are used to make up the actual carrier service may have different carrier configurations.
       * A specific carrier ID could also be used, for example, in a scenario where a carrier requires
       * different carrier configuration for different service offering such as a prepaid plan.
       * e.g, {@link #getCarrierId()} will always return Tracfone (id 2022) for a Tracfone SIM, while
       * {@link #getSpecificCarrierId()} can return Tracfone AT&T or Tracfone T-Mobile based on the
       * IMSI from the current subscription.
       *
       * For carriers without any fine-grained carrier ids, return {@link #getCarrierId()}
       */
      public int getSpecificCarrierId() {
          return mSpecificCarrierId;
      }
  
      public String getCarrierName() {
          return mCarrierName;
      }
  
      public String getSpecificCarrierName() {
          return mSpecificCarrierName;
      }
  
      public int getMnoCarrierId() {
          return mMnoCarrierId;
      }
  
      /**
       * a util function to convert carrierIdentifier to the best matching carrier id.
       *
       * @return the best matching carrier id.
       */
      public static int getCarrierIdFromIdentifier(@NonNull Context context,
                                                   @NonNull CarrierIdentifier carrierIdentifier) {
          final String mccmnc = carrierIdentifier.getMcc() + carrierIdentifier.getMnc();
          final String gid1 = carrierIdentifier.getGid1();
          final String gid2 = carrierIdentifier.getGid2();
          final String imsi = carrierIdentifier.getImsi();
          final String spn = carrierIdentifier.getSpn();
          if (VDBG) {
              logd("[getCarrierIdFromIdentifier]"
                      + " mnnmnc:" + mccmnc
                      + " gid1: " + gid1
                      + " gid2: " + gid2
                      + " imsi: " + Rlog.pii(LOG_TAG, imsi)
                      + " spn: " + spn);
          }
          // assign null to other fields which are not supported by carrierIdentifier.
          CarrierMatchingRule targetRule =
                  new CarrierMatchingRule(mccmnc, imsi, null, gid1, gid2, null,
                          spn, null, null,
                          TelephonyManager.UNKNOWN_CARRIER_ID_LIST_VERSION, null,
                          TelephonyManager.UNKNOWN_CARRIER_ID);
  
          int carrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
          int maxScore = CarrierMatchingRule.SCORE_INVALID;
          List<CarrierMatchingRule> rules = getCarrierMatchingRulesFromMccMnc(
                  context, targetRule.mccMnc);
          for (CarrierMatchingRule rule : rules) {
              rule.match(targetRule);
              if (rule.mScore > maxScore) {
                  maxScore = rule.mScore;
                  carrierId = rule.mCid;
              }
          }
          return carrierId;
      }
  
      /**
       * a util function to convert {mccmnc, mvno_type, mvno_data} to all matching carrier ids.
       *
       * @return a list of id with matching {mccmnc, mvno_type, mvno_data}
       */
      public static List<Integer> getCarrierIdsFromApnQuery(@NonNull Context context,
                                                            String mccmnc, String mvnoCase,
                                                            String mvnoData) {
          String selection = CarrierId.All.MCCMNC + "=" + mccmnc;
          // build the proper query
          if ("spn".equals(mvnoCase) && mvnoData != null) {
              selection += " AND " + CarrierId.All.SPN + "='" + mvnoData + "'";
          } else if ("imsi".equals(mvnoCase) && mvnoData != null) {
              selection += " AND " + CarrierId.All.IMSI_PREFIX_XPATTERN + "='" + mvnoData + "'";
          } else if ("gid1".equals(mvnoCase) && mvnoData != null) {
              selection += " AND " + CarrierId.All.GID1 + "='" + mvnoData + "'";
          } else if ("gid2".equals(mvnoCase) && mvnoData != null) {
              selection += " AND " + CarrierId.All.GID2 + "='" + mvnoData +"'";
          } else {
              logd("mvno case empty or other invalid values");
          }
  
          List<Integer> ids = new ArrayList<>();
          try {
              Cursor cursor = context.getContentResolver().query(
                      CarrierId.All.CONTENT_URI,
                      /* projection */ null,
                      /* selection */ selection,
                      /* selectionArgs */ null, null);
              try {
                  if (cursor != null) {
                      if (VDBG) {
                          logd("[getCarrierIdsFromApnQuery]- " + cursor.getCount()
                                  + " Records(s) in DB");
                      }
                      while (cursor.moveToNext()) {
                          int cid = cursor.getInt(cursor.getColumnIndex(CarrierId.CARRIER_ID));
                          if (!ids.contains(cid)) {
                              ids.add(cid);
                          }
                      }
                  }
              } finally {
                  if (cursor != null) {
                      cursor.close();
                  }
              }
          } catch (Exception ex) {
              loge("[getCarrierIdsFromApnQuery]- ex: " + ex);
          }
          logd(selection + " " + ids);
          return ids;
      }
  
      // static helper function to get carrier id from mccmnc
      public static int getCarrierIdFromMccMnc(@NonNull Context context, String mccmnc) {
          try {
              Cursor cursor = context.getContentResolver().query(
                      CarrierId.All.CONTENT_URI,
                      /* projection */ null,
                      /* selection */ CarrierId.All.MCCMNC + "=? AND "
                              + CarrierId.All.GID1 + " is NULL AND "
                              + CarrierId.All.GID2 + " is NULL AND "
                              + CarrierId.All.IMSI_PREFIX_XPATTERN + " is NULL AND "
                              + CarrierId.All.SPN + " is NULL AND "
                              + CarrierId.All.ICCID_PREFIX + " is NULL AND "
                              + CarrierId.All.PLMN + " is NULL AND "
                              + CarrierId.All.PRIVILEGE_ACCESS_RULE + " is NULL AND "
                              + CarrierId.All.APN + " is NULL",
                      /* selectionArgs */ new String[]{mccmnc},
                      null);
              try {
                  if (cursor != null) {
                      if (VDBG) {
                          logd("[getCarrierIdFromMccMnc]- " + cursor.getCount()
                                  + " Records(s) in DB" + " mccmnc: " + mccmnc);
                      }
                      while (cursor.moveToNext()) {
                          return cursor.getInt(cursor.getColumnIndex(CarrierId.CARRIER_ID));
                      }
                  }
              } finally {
                  if (cursor != null) {
                      cursor.close();
                  }
              }
          } catch (Exception ex) {
              loge("[getCarrierIdFromMccMnc]- ex: " + ex);
          }
          return TelephonyManager.UNKNOWN_CARRIER_ID;
      }
  
      private static boolean equals(String a, String b, boolean ignoreCase) {
          if (a == null && b == null) return true;
          if (a != null && b != null) {
              return (ignoreCase) ? a.equalsIgnoreCase(b) : a.equals(b);
          }
          return false;
      }
  
      private static void logd(String str) {
          Rlog.d(LOG_TAG, str);
      }
      private static void loge(String str) {
          Rlog.e(LOG_TAG, str);
      }
      public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
          final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
          ipw.println("mCarrierResolverLocalLogs:");
          ipw.increaseIndent();
          mCarrierIdLocalLog.dump(fd, pw, args);
          ipw.decreaseIndent();
  
          ipw.println("mCarrierId: " + mCarrierId);
          ipw.println("mSpecificCarrierId: " + mSpecificCarrierId);
          ipw.println("mMnoCarrierId: " + mMnoCarrierId);
          ipw.println("mCarrierName: " + mCarrierName);
          ipw.println("mSpecificCarrierName: " + mSpecificCarrierName);
          ipw.println("carrier_list_version: " + getCarrierListVersion());
  
          ipw.println("mCarrierMatchingRules on mccmnc: "
                  + mTelephonyMgr.getSimOperatorNumericForPhone(mPhone.getPhoneId()));
          ipw.increaseIndent();
          for (CarrierMatchingRule rule : mCarrierMatchingRulesOnMccMnc) {
              ipw.println(rule.toString());
          }
          ipw.decreaseIndent();
  
          ipw.println("mSpn: " + mSpn);
          ipw.println("mPreferApn: " + mPreferApn);
          ipw.flush();
      }
  }
  