content: /*
   * Copyright (C) 2018 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.uicc.euicc;
  
  import android.annotation.Nullable;
  import android.content.Context;
  import android.content.res.Resources;
  import android.os.AsyncResult;
  import android.os.Handler;
  import android.os.Registrant;
  import android.os.RegistrantList;
  import android.service.carrier.CarrierIdentifier;
  import android.service.euicc.EuiccProfileInfo;
  import android.telephony.Rlog;
  import android.telephony.SubscriptionInfo;
  import android.telephony.UiccAccessRule;
  import android.telephony.euicc.EuiccCardManager;
  import android.telephony.euicc.EuiccNotification;
  import android.telephony.euicc.EuiccRulesAuthTable;
  import android.text.TextUtils;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.telephony.CommandsInterface;
  import com.android.internal.telephony.Phone;
  import com.android.internal.telephony.PhoneFactory;
  import com.android.internal.telephony.uicc.IccCardStatus;
  import com.android.internal.telephony.uicc.IccIoResult;
  import com.android.internal.telephony.uicc.IccUtils;
  import com.android.internal.telephony.uicc.UiccCard;
  import com.android.internal.telephony.uicc.asn1.Asn1Decoder;
  import com.android.internal.telephony.uicc.asn1.Asn1Node;
  import com.android.internal.telephony.uicc.asn1.InvalidAsn1DataException;
  import com.android.internal.telephony.uicc.asn1.TagNotFoundException;
  import com.android.internal.telephony.uicc.euicc.EuiccCardErrorException.OperationCode;
  import com.android.internal.telephony.uicc.euicc.apdu.ApduException;
  import com.android.internal.telephony.uicc.euicc.apdu.ApduSender;
  import com.android.internal.telephony.uicc.euicc.apdu.ApduSenderResultCallback;
  import com.android.internal.telephony.uicc.euicc.apdu.RequestBuilder;
  import com.android.internal.telephony.uicc.euicc.apdu.RequestProvider;
  import com.android.internal.telephony.uicc.euicc.async.AsyncResultCallback;
  import com.android.internal.telephony.uicc.euicc.async.AsyncResultHelper;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.Arrays;
  import java.util.List;
  
  /**
   * This represents an eUICC card to perform profile management operations asynchronously. This class
   * includes methods defined by different versions of GSMA Spec (SGP.22).
   */
  public class EuiccCard extends UiccCard {
      private static final String LOG_TAG = "EuiccCard";
      private static final boolean DBG = true;
  
      private static final String ISD_R_AID = "A0000005591010FFFFFFFF8900000100";
      private static final int ICCID_LENGTH = 20;
  
      // APDU status for SIM refresh
      private static final int APDU_ERROR_SIM_REFRESH = 0x6F00;
  
      // These error codes are defined in GSMA SGP.22. 0 is the code for success.
      private static final int CODE_OK = 0;
  
      // Error code for profile not in expected state for the operation. This error includes the case
      // that profile is not in disabled state when being enabled or deleted, and that profile is not
      // in enabled state when being disabled.
      private static final int CODE_PROFILE_NOT_IN_EXPECTED_STATE = 2;
  
      // Error code for nothing to delete when resetting eUICC memory or removing notifications.
      private static final int CODE_NOTHING_TO_DELETE = 1;
  
      // Error code for no result available when retrieving notifications.
      private static final int CODE_NO_RESULT_AVAILABLE = 1;
  
      private static final EuiccSpecVersion SGP22_V_2_0 = new EuiccSpecVersion(2, 0, 0);
      private static final EuiccSpecVersion SGP22_V_2_1 = new EuiccSpecVersion(2, 1, 0);
  
      // Device capabilities.
      private static final String DEV_CAP_GSM = "gsm";
      private static final String DEV_CAP_UTRAN = "utran";
      private static final String DEV_CAP_CDMA_1X = "cdma1x";
      private static final String DEV_CAP_HRPD = "hrpd";
      private static final String DEV_CAP_EHRPD = "ehrpd";
      private static final String DEV_CAP_EUTRAN = "eutran";
      private static final String DEV_CAP_NFC = "nfc";
      private static final String DEV_CAP_CRL = "crl";
  
      // These interfaces are used for simplifying the code by leveraging lambdas.
      private interface ApduRequestBuilder {
          void build(RequestBuilder requestBuilder)
                  throws EuiccCardException, TagNotFoundException, InvalidAsn1DataException;
      }
  
      private interface ApduResponseHandler<T> {
          T handleResult(byte[] response)
                  throws EuiccCardException, TagNotFoundException, InvalidAsn1DataException;
      }
  
      private interface ApduIntermediateResultHandler {
          boolean shouldContinue(IccIoResult intermediateResult);
      }
  
      private interface ApduExceptionHandler {
          void handleException(Throwable e);
      }
  
      private final ApduSender mApduSender;
      private RegistrantList mEidReadyRegistrants;
      private EuiccSpecVersion mSpecVersion;
      private volatile String mEid;
  
      public EuiccCard(Context c, CommandsInterface ci, IccCardStatus ics, int phoneId, Object lock) {
          super(c, ci, ics, phoneId, lock);
          // TODO: Set supportExtendedApdu based on ATR.
          mApduSender = new ApduSender(ci, ISD_R_AID, false /* supportExtendedApdu */);
  
          if (TextUtils.isEmpty(ics.eid)) {
              loge("no eid given in constructor for phone " + phoneId);
              loadEidAndNotifyRegistrants();
          } else {
              mEid = ics.eid;
              mCardId = ics.eid;
          }
      }
  
      /**
       * Registers to be notified when EID is ready. If the EID is ready when this method is called,
       * the registrant will be notified immediately.
       */
      public void registerForEidReady(Handler h, int what, Object obj) {
          Registrant r = new Registrant(h, what, obj);
          if (mEid != null) {
              r.notifyRegistrant(new AsyncResult(null, null, null));
          } else {
              if (mEidReadyRegistrants == null) {
                  mEidReadyRegistrants = new RegistrantList();
              }
              mEidReadyRegistrants.add(r);
          }
      }
  
      /**
       * Unregisters to be notified when EID is ready.
       */
      public void unregisterForEidReady(Handler h) {
          if (mEidReadyRegistrants != null) {
              mEidReadyRegistrants.remove(h);
          }
      }
  
      // For RadioConfig<1.2 we don't know the EID when constructing the EuiccCard, so callers may
      // need to register to be notified when we have the EID
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
      protected void loadEidAndNotifyRegistrants() {
          Handler euiccMainThreadHandler = new Handler();
          AsyncResultCallback<String> cardCb = new AsyncResultCallback<String>() {
              @Override
              public void onResult(String result) {
                  if (mEidReadyRegistrants != null) {
                      mEidReadyRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
                  }
              }
  
              @Override
              public void onException(Throwable e) {
                  // Still notifying registrants even getting eid fails.
                  if (mEidReadyRegistrants != null) {
                      mEidReadyRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
                  }
                  mEid = "";
                  mCardId = "";
                  Rlog.e(LOG_TAG, "Failed loading eid", e);
              }
          };
          getEid(cardCb, euiccMainThreadHandler);
      }
  
      /**
       * Gets the GSMA RSP specification version supported by this eUICC. This may return null if the
       * version cannot be read.
       */
      public void getSpecVersion(AsyncResultCallback<EuiccSpecVersion> callback, Handler handler) {
          if (mSpecVersion != null) {
              AsyncResultHelper.returnResult(mSpecVersion, callback, handler);
              return;
          }
  
          sendApdu(newRequestProvider((RequestBuilder requestBuilder) -> { /* Do nothing */ }),
                  (byte[] response) -> mSpecVersion, callback, handler);
      }
  
      @Override
      public void update(Context c, CommandsInterface ci, IccCardStatus ics) {
          synchronized (mLock) {
              if (!TextUtils.isEmpty(ics.eid)) {
                  mEid = ics.eid;
              }
              super.update(c, ci, ics);
          }
      }
  
      @Override
      protected void updateCardId() {
          if (TextUtils.isEmpty(mEid)) {
              super.updateCardId();
          } else {
              mCardId = mEid;
          }
      }
  
      /**
       * Gets a list of user-visible profiles.
       *
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 1.1.0 [GSMA SGP.22]
       */
      public void getAllProfiles(AsyncResultCallback<EuiccProfileInfo[]> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_PROFILES)
                                  .addChildAsBytes(Tags.TAG_TAG_LIST, Tags.EUICC_PROFILE_TAGS)
                                  .build().toHex())),
                  response -> {
                      List<Asn1Node> profileNodes = new Asn1Decoder(response).nextNode()
                              .getChild(Tags.TAG_CTX_COMP_0).getChildren(Tags.TAG_PROFILE_INFO);
                      int size = profileNodes.size();
                      EuiccProfileInfo[] profiles = new EuiccProfileInfo[size];
                      int profileCount = 0;
                      for (int i = 0; i < size; i++) {
                          Asn1Node profileNode = profileNodes.get(i);
                          if (!profileNode.hasChild(Tags.TAG_ICCID)) {
                              loge("Profile must have an ICCID.");
                              continue;
                          }
                          String strippedIccIdString =
                                  stripTrailingFs(profileNode.getChild(Tags.TAG_ICCID).asBytes());
                          EuiccProfileInfo.Builder profileBuilder =
                                  new EuiccProfileInfo.Builder(strippedIccIdString);
                          buildProfile(profileNode, profileBuilder);
  
                          EuiccProfileInfo profile = profileBuilder.build();
                          profiles[profileCount++] = profile;
                      }
                      return profiles;
                  },
                  callback, handler);
      }
  
      /**
       * Gets a profile.
       *
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 1.1.0 [GSMA SGP.22]
       */
      public final void getProfile(String iccid, AsyncResultCallback<EuiccProfileInfo> callback,
              Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_PROFILES)
                                  .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
                                      .addChildAsBytes(
                                          Tags.TAG_ICCID, IccUtils.bcdToBytes(padTrailingFs(iccid)))
                                      .build())
                                  .addChildAsBytes(Tags.TAG_TAG_LIST, Tags.EUICC_PROFILE_TAGS)
                                  .build().toHex())),
                  response -> {
                      List<Asn1Node> profileNodes = new Asn1Decoder(response).nextNode()
                              .getChild(Tags.TAG_CTX_COMP_0).getChildren(Tags.TAG_PROFILE_INFO);
                      if (profileNodes.isEmpty()) {
                          return null;
                      }
                      Asn1Node profileNode = profileNodes.get(0);
                      String strippedIccIdString =
                              stripTrailingFs(profileNode.getChild(Tags.TAG_ICCID).asBytes());
                      EuiccProfileInfo.Builder profileBuilder =
                              new EuiccProfileInfo.Builder(strippedIccIdString);
                      buildProfile(profileNode, profileBuilder);
                      return profileBuilder.build();
                  },
                  callback, handler);
      }
  
      /**
       * Disables a profile of the given {@code iccid}.
       *
       * @param refresh Whether sending the REFRESH command to modem.
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 1.1.0 [GSMA SGP.22]
       */
      public void disableProfile(String iccid, boolean refresh, AsyncResultCallback<Void> callback,
              Handler handler) {
          sendApduWithSimResetErrorWorkaround(
                  newRequestProvider((RequestBuilder requestBuilder) -> {
                      byte[] iccidBytes = IccUtils.bcdToBytes(padTrailingFs(iccid));
                      requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_DISABLE_PROFILE)
                              .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
                                      .addChildAsBytes(Tags.TAG_ICCID, iccidBytes))
                              .addChildAsBoolean(Tags.TAG_CTX_1, refresh)
                              .build().toHex());
                  }),
                  response -> {
                      int result;
                      // SGP.22 v2.0 DisableProfileResponse
                      result = parseSimpleResult(response);
                      switch (result) {
                          case CODE_OK:
                              return null;
                          case CODE_PROFILE_NOT_IN_EXPECTED_STATE:
                              logd("Profile is already disabled, iccid: "
                                      + SubscriptionInfo.givePrintableIccid(iccid));
                              return null;
                          default:
                              throw new EuiccCardErrorException(
                                      EuiccCardErrorException.OPERATION_DISABLE_PROFILE, result);
                      }
                  },
                  callback, handler);
      }
  
      /**
       * Switches from the current profile to another profile. The current profile will be disabled
       * and the specified profile will be enabled.
       *
       * @param refresh Whether sending the REFRESH command to modem.
       * @param callback The callback to get the EuiccProfile enabled.
       * @param handler The handler to run the callback.
       * @since 1.1.0 [GSMA SGP.22]
       */
      public void switchToProfile(String iccid, boolean refresh, AsyncResultCallback<Void> callback,
              Handler handler) {
          sendApduWithSimResetErrorWorkaround(
                  newRequestProvider((RequestBuilder requestBuilder) -> {
                      byte[] iccidBytes = IccUtils.bcdToBytes(padTrailingFs(iccid));
                      requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_ENABLE_PROFILE)
                              .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
                                      .addChildAsBytes(Tags.TAG_ICCID, iccidBytes))
                              .addChildAsBoolean(Tags.TAG_CTX_1, refresh)
                              .build().toHex());
                  }),
                  response -> {
                      int result;
                      // SGP.22 v2.0 EnableProfileResponse
                      result = parseSimpleResult(response);
                      switch (result) {
                          case CODE_OK:
                              return null;
                          case CODE_PROFILE_NOT_IN_EXPECTED_STATE:
                              logd("Profile is already enabled, iccid: "
                                      + SubscriptionInfo.givePrintableIccid(iccid));
                              return null;
                          default:
                              throw new EuiccCardErrorException(
                                      EuiccCardErrorException.OPERATION_SWITCH_TO_PROFILE, result);
                      }
                  },
                  callback, handler);
      }
  
      /**
       * Gets the EID synchronously.
       * @return The EID string. Returns null if it is not ready yet.
       */
      public String getEid() {
          return mEid;
      }
  
      /**
       * Gets the EID of the eUICC and overwrites mCardId in UiccCard.
       *
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 1.1.0 [GSMA SGP.22]
       */
      public void getEid(AsyncResultCallback<String> callback, Handler handler) {
          if (mEid != null) {
              AsyncResultHelper.returnResult(mEid, callback, handler);
              return;
          }
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_EID)
                                  .addChildAsBytes(Tags.TAG_TAG_LIST, new byte[] {Tags.TAG_EID})
                                  .build().toHex())),
                  response -> {
                      String eid = IccUtils.bytesToHexString(parseResponse(response)
                              .getChild(Tags.TAG_EID).asBytes());
                      synchronized (mLock) {
                          mEid = eid;
                          mCardId = eid;
                      }
                      return eid;
                  },
                  callback, handler);
      }
  
      /**
       * Sets the nickname of a profile.
       *
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 1.1.0 [GSMA SGP.22]
       */
      public void setNickname(String iccid, String nickname, AsyncResultCallback<Void> callback,
              Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_SET_NICKNAME)
                                  .addChildAsBytes(Tags.TAG_ICCID,
                                          IccUtils.bcdToBytes(padTrailingFs(iccid)))
                                  .addChildAsString(Tags.TAG_NICKNAME, nickname)
                                  .build().toHex())),
                  response -> {
                      // SGP.22 v2.0 SetNicknameResponse
                      int result = parseSimpleResult(response);
                      if (result != CODE_OK) {
                          throw new EuiccCardErrorException(
                                  EuiccCardErrorException.OPERATION_SET_NICKNAME, result);
                      }
                      return null;
                  },
                  callback, handler);
      }
  
      /**
       * Deletes a profile from eUICC.
       *
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 1.1.0 [GSMA SGP.22]
       */
      public void deleteProfile(String iccid, AsyncResultCallback<Void> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) -> {
                      byte[] iccidBytes = IccUtils.bcdToBytes(padTrailingFs(iccid));
                      requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_DELETE_PROFILE)
                              .addChildAsBytes(Tags.TAG_ICCID, iccidBytes)
                              .build().toHex());
                  }),
                  response -> {
                      // SGP.22 v2.0 DeleteProfileRequest
                      int result = parseSimpleResult(response);
                      if (result != CODE_OK) {
                          throw new EuiccCardErrorException(
                                  EuiccCardErrorException.OPERATION_DELETE_PROFILE, result);
                      }
                      return null;
                  },
                  callback, handler);
      }
  
      /**
       * Resets the eUICC memory (e.g., remove all profiles).
       *
       * @param options Bits of the options of resetting which parts of the eUICC memory.
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 1.1.0 [GSMA SGP.22]
       */
      public void resetMemory(@EuiccCardManager.ResetOption int options,
              AsyncResultCallback<Void> callback, Handler handler) {
          sendApduWithSimResetErrorWorkaround(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_EUICC_MEMORY_RESET)
                                  .addChildAsBits(Tags.TAG_CTX_2, options)
                                  .build().toHex())),
                  response -> {
                      int result = parseSimpleResult(response);
                      if (result != CODE_OK && result != CODE_NOTHING_TO_DELETE) {
                          throw new EuiccCardErrorException(
                                  EuiccCardErrorException.OPERATION_RESET_MEMORY, result);
                      }
                      return null;
                  },
                  callback, handler);
      }
  
      /**
       * Gets the default SM-DP+ address from eUICC.
       *
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void getDefaultSmdpAddress(AsyncResultCallback<String> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(
                                  Asn1Node.newBuilder(Tags.TAG_GET_CONFIGURED_ADDRESSES)
                                          .build().toHex())),
                  (byte[] response) -> parseResponse(response).getChild(Tags.TAG_CTX_0).asString(),
                  callback, handler);
      }
  
      /**
       * Gets the SM-DS address from eUICC.
       *
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void getSmdsAddress(AsyncResultCallback<String> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(
                                  Asn1Node.newBuilder(Tags.TAG_GET_CONFIGURED_ADDRESSES)
                                          .build().toHex())),
                  (byte[] response) -> parseResponse(response).getChild(Tags.TAG_CTX_1).asString(),
                  callback, handler);
      }
  
      /**
       * Sets the default SM-DP+ address of eUICC.
       *
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void setDefaultSmdpAddress(String defaultSmdpAddress, AsyncResultCallback<Void> callback,
              Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(
                                  Asn1Node.newBuilder(Tags.TAG_SET_DEFAULT_SMDP_ADDRESS)
                                          .addChildAsString(Tags.TAG_CTX_0, defaultSmdpAddress)
                                          .build().toHex())),
                  response -> {
                      // SGP.22 v2.0 SetDefaultDpAddressResponse
                      int result = parseSimpleResult(response);
                      if (result != CODE_OK) {
                          throw new EuiccCardErrorException(
                                  EuiccCardErrorException.OPERATION_SET_DEFAULT_SMDP_ADDRESS, result);
                      }
                      return null;
                  },
                  callback, handler);
      }
  
      /**
       * Gets Rules Authorisation Table.
       *
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void getRulesAuthTable(AsyncResultCallback<EuiccRulesAuthTable> callback,
              Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_RAT)
                                  .build().toHex())),
                  response -> {
                      Asn1Node root = parseResponse(response);
                      List<Asn1Node> nodes = root.getChildren(Tags.TAG_CTX_COMP_0);
                      EuiccRulesAuthTable.Builder builder =
                              new EuiccRulesAuthTable.Builder(nodes.size());
                      int size = nodes.size();
                      for (int i = 0; i < size; i++) {
                          Asn1Node node = nodes.get(i);
                          List<Asn1Node> opIdNodes =
                                  node.getChild(Tags.TAG_SEQUENCE, Tags.TAG_CTX_COMP_1).getChildren();
                          int opIdSize = opIdNodes.size();
                          CarrierIdentifier[] opIds = new CarrierIdentifier[opIdSize];
                          for (int j = 0; j < opIdSize; j++) {
                              opIds[j] = buildCarrierIdentifier(opIdNodes.get(j));
                          }
                          builder.add(node.getChild(Tags.TAG_SEQUENCE, Tags.TAG_CTX_0).asBits(),
                                  Arrays.asList(opIds), node.getChild(Tags.TAG_SEQUENCE,
                                  Tags.TAG_CTX_2).asBits());
                      }
                      return builder.build();
                  },
                  callback, handler);
      }
  
      /**
       * Gets the eUICC challenge for new profile downloading.
       *
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void getEuiccChallenge(AsyncResultCallback<byte[]> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(
                                  Asn1Node.newBuilder(Tags.TAG_GET_EUICC_CHALLENGE)
                                          .build().toHex())),
                  (byte[] response) -> parseResponse(response).getChild(Tags.TAG_CTX_0).asBytes(),
                  callback, handler);
      }
  
      /**
       * Gets the eUICC info1 for new profile downloading.
       *
       * @param callback The callback to get the result, which represents an {@code EUICCInfo1}
       *     defined in GSMA RSP v2.0+.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void getEuiccInfo1(AsyncResultCallback<byte[]> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_EUICC_INFO_1)
                                  .build().toHex())),
                  (response) -> response,
                  callback, handler);
      }
  
      /**
       * Gets the eUICC info2 for new profile downloading.
       *
       * @param callback The callback to get the result, which represents an {@code EUICCInfo2}
       *     defined in GSMA RSP v2.0+.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void getEuiccInfo2(AsyncResultCallback<byte[]> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_EUICC_INFO_2)
                                  .build().toHex())),
                  (response) -> response,
                  callback, handler);
      }
  
      /**
       * Authenticates the SM-DP+ server by the eUICC. The parameters {@code serverSigned1}, {@code
       * serverSignature1}, {@code euiccCiPkIdToBeUsed}, and {@code serverCertificate} are the ASN.1
       * data returned by SM-DP+ server.
       *
       * @param matchingId The activation code or an empty string.
       * @param callback The callback to get the result, which represents an {@code
       *     AuthenticateServerResponse} defined in GSMA RSP v2.0+.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void authenticateServer(String matchingId, byte[] serverSigned1, byte[] serverSignature1,
              byte[] euiccCiPkIdToBeUsed, byte[] serverCertificate,
              AsyncResultCallback<byte[]> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) -> {
                      byte[] imeiBytes = getDeviceId();
                      // TAC is the first 8 digits (4 bytes) of IMEI.
                      byte[] tacBytes = new byte[4];
                      System.arraycopy(imeiBytes, 0, tacBytes, 0, 4);
  
                      Asn1Node.Builder devCapsBuilder = Asn1Node.newBuilder(Tags.TAG_CTX_COMP_1);
                      String[] devCapsStrings = getResources().getStringArray(
                              com.android.internal.R.array.config_telephonyEuiccDeviceCapabilities);
                      if (devCapsStrings != null) {
                          for (String devCapItem : devCapsStrings) {
                              addDeviceCapability(devCapsBuilder, devCapItem);
                          }
                      } else {
                          if (DBG) logd("No device capabilities set.");
                      }
  
                      Asn1Node.Builder ctxParams1Builder = Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
                              .addChildAsString(Tags.TAG_CTX_0, matchingId)
                              .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_1)
                                      .addChildAsBytes(Tags.TAG_CTX_0, tacBytes)
                                      .addChild(devCapsBuilder)
                                      .addChildAsBytes(Tags.TAG_CTX_2, imeiBytes));
  
                      requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_AUTHENTICATE_SERVER)
                              .addChild(new Asn1Decoder(serverSigned1).nextNode())
                              .addChild(new Asn1Decoder(serverSignature1).nextNode())
                              .addChild(new Asn1Decoder(euiccCiPkIdToBeUsed).nextNode())
                              .addChild(new Asn1Decoder(serverCertificate).nextNode())
                              .addChild(ctxParams1Builder)
                              .build().toHex());
                  }),
                  response -> {
                      Asn1Node root = parseResponse(response);
                      if (root.hasChild(Tags.TAG_CTX_COMP_1, Tags.TAG_UNI_2)) {
                          throw new EuiccCardErrorException(
                                  EuiccCardErrorException.OPERATION_AUTHENTICATE_SERVER,
                                  root.getChild(Tags.TAG_CTX_COMP_1, Tags.TAG_UNI_2).asInteger());
                      }
                      return root.toBytes();
                  },
                  callback, handler);
      }
  
      /**
       * Prepares the profile download request sent to SM-DP+. The parameters {@code smdpSigned2},
       * {@code smdpSignature2}, and {@code smdpCertificate} are the ASN.1 data returned by SM-DP+
       * server.
       *
       * @param hashCc The hash of confirmation code. It can be null if there is no confirmation code
       *     required.
       * @param callback The callback to get the result, which represents an {@code
       *     PrepareDownloadResponse} defined in GSMA RSP v2.0+.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void prepareDownload(@Nullable byte[] hashCc, byte[] smdpSigned2, byte[] smdpSignature2,
              byte[] smdpCertificate, AsyncResultCallback<byte[]> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) -> {
                      Asn1Node.Builder builder = Asn1Node.newBuilder(Tags.TAG_PREPARE_DOWNLOAD)
                              .addChild(new Asn1Decoder(smdpSigned2).nextNode())
                              .addChild(new Asn1Decoder(smdpSignature2).nextNode());
                      if (hashCc != null) {
                          builder.addChildAsBytes(Tags.TAG_UNI_4, hashCc);
                      }
                      requestBuilder.addStoreData(
                              builder.addChild(new Asn1Decoder(smdpCertificate).nextNode())
                                      .build().toHex());
                  }),
                  response -> {
                      Asn1Node root = parseResponse(response);
                      if (root.hasChild(Tags.TAG_CTX_COMP_1, Tags.TAG_UNI_2)) {
                          throw new EuiccCardErrorException(
                                  EuiccCardErrorException.OPERATION_PREPARE_DOWNLOAD,
                                  root.getChild(Tags.TAG_CTX_COMP_1, Tags.TAG_UNI_2).asInteger());
                      }
                      return root.toBytes();
                  },
                  callback, handler);
      }
  
      /**
       * Loads a downloaded bound profile package onto the eUICC.
       *
       * @param boundProfilePackage The Bound Profile Package data returned by SM-DP+ server.
       * @param callback The callback to get the result, which represents an {@code
       *     LoadBoundProfilePackageResponse} defined in GSMA RSP v2.0+.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void loadBoundProfilePackage(byte[] boundProfilePackage,
              AsyncResultCallback<byte[]> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) -> {
                      Asn1Node bppNode = new Asn1Decoder(boundProfilePackage).nextNode();
                      int actualLength = bppNode.getDataLength();
                      int segmentedLength = 0;
                      // initialiseSecureChannelRequest (ES8+.InitialiseSecureChannel)
                      Asn1Node initialiseSecureChannelRequest = bppNode.getChild(
                              Tags.TAG_INITIALISE_SECURE_CHANNEL);
                      segmentedLength += initialiseSecureChannelRequest.getEncodedLength();
                      // firstSequenceOf87 (ES8+.ConfigureISDP)
                      Asn1Node firstSequenceOf87 = bppNode.getChild(Tags.TAG_CTX_COMP_0);
                      segmentedLength += firstSequenceOf87.getEncodedLength();
                      // sequenceOf88 (ES8+.StoreMetadata)
                      Asn1Node sequenceOf88 = bppNode.getChild(Tags.TAG_CTX_COMP_1);
                      List<Asn1Node> metaDataSeqs = sequenceOf88.getChildren(Tags.TAG_CTX_8);
                      segmentedLength += sequenceOf88.getEncodedLength();
                      // secondSequenceOf87 (ES8+.ReplaceSessionKeys), optional
                      Asn1Node secondSequenceOf87 = null;
                      if (bppNode.hasChild(Tags.TAG_CTX_COMP_2)) {
                          secondSequenceOf87 = bppNode.getChild(Tags.TAG_CTX_COMP_2);
                          segmentedLength += secondSequenceOf87.getEncodedLength();
                      }
                      // sequenceOf86 (ES8+.LoadProfileElements)
                      Asn1Node sequenceOf86 = bppNode.getChild(Tags.TAG_CTX_COMP_3);
                      List<Asn1Node> elementSeqs = sequenceOf86.getChildren(Tags.TAG_CTX_6);
                      segmentedLength += sequenceOf86.getEncodedLength();
  
                      if (mSpecVersion.compareTo(SGP22_V_2_1) >= 0) {
                          // Per SGP.22 v2.1+ section 2.5.5, it's the LPA's job to "segment" the BPP
                          // before sending it to the eUICC. This check was only instituted in SGP.22
                          // v2.1 and higher. SGP.22 v2.0 doesn't mention this "segmentation" process
                          // at all, or what the LPA should do in the case of unrecognized or missing
                          // tags. Per section 3.1.3.3: "If the LPAd is unable to perform the
                          // segmentation (e.g., because of an error in the BPP structure), ... the
                          // LPAd SHALL perform the Sub-procedure "Profile Download and installation -
                          // Download rejection" with reason code 'Load BPP execution error'." This
                          // implies that if we detect an invalid BPP, we should short-circuit before
                          // sending anything to the eUICC. There are two cases to account for:
                          if (elementSeqs == null || elementSeqs.isEmpty()) {
                              // 1. The BPP is missing a required tag. Upon calling bppNode.getChild,
                              // an exception will occur if the expected tag is missing, though we
                              // should make sure that the sequences are non-empty when appropriate as
                              // well. A profile with no profile elements is invalid. This is
                              // explicitly tested by SGP.23 case 4.4.25.2.1_03.
                              throw new EuiccCardException("No profile elements in BPP");
                          } else if (actualLength != segmentedLength) {
                              // 2. The BPP came with extraneous tags other than what the spec
                              // mandates. We keep track of the total length of the BPP and compare it
                              // to the length of the segments we care about. If they're different,
                              // we'll throw an exception to indicate this. This is explicitly tested
                              // by SGP.23 case 4.4.25.2.1_05.
                              throw new EuiccCardException(
                                      "Actual BPP length ("
                                              + actualLength
                                              + ") does not match segmented length ("
                                              + segmentedLength
                                              + "), this must be due to a malformed BPP");
                          }
                      }
  
                      requestBuilder.addStoreData(bppNode.getHeadAsHex()
                              + initialiseSecureChannelRequest.toHex());
  
                      requestBuilder.addStoreData(firstSequenceOf87.toHex());
  
                      requestBuilder.addStoreData(sequenceOf88.getHeadAsHex());
                      int size = metaDataSeqs.size();
                      for (int i = 0; i < size; i++) {
                          requestBuilder.addStoreData(metaDataSeqs.get(i).toHex());
                      }
  
                      if (secondSequenceOf87 != null) {
                          requestBuilder.addStoreData(secondSequenceOf87.toHex());
                      }
  
                      requestBuilder.addStoreData(sequenceOf86.getHeadAsHex());
                      size = elementSeqs.size();
                      for (int i = 0; i < size; i++) {
                          requestBuilder.addStoreData(elementSeqs.get(i).toHex());
                      }
                  }),
                  response -> {
                      // SGP.22 v2.0 ErrorResult
                      Asn1Node root = parseResponse(response);
                      if (root.hasChild(Tags.TAG_PROFILE_INSTALLATION_RESULT_DATA,
                              Tags.TAG_CTX_COMP_2, Tags.TAG_CTX_COMP_1, Tags.TAG_CTX_1)) {
                          Asn1Node errorNode = root.getChild(
                                  Tags.TAG_PROFILE_INSTALLATION_RESULT_DATA, Tags.TAG_CTX_COMP_2,
                                  Tags.TAG_CTX_COMP_1, Tags.TAG_CTX_1);
                          throw new EuiccCardErrorException(
                                  EuiccCardErrorException.OPERATION_LOAD_BOUND_PROFILE_PACKAGE,
                                  errorNode.asInteger(), errorNode);
                      }
                      return root.toBytes();
                  },
                  intermediateResult -> {
                      byte[] payload = intermediateResult.payload;
                      if (payload != null && payload.length > 2) {
                          int tag = (payload[0] & 0xFF) << 8 | (payload[1] & 0xFF);
                          // Stops if the installation result has been returned
                          if (tag == Tags.TAG_PROFILE_INSTALLATION_RESULT) {
                              logd("loadBoundProfilePackage failed due to an early error.");
                              return false;
                          }
                      }
                      return true;
                  },
                  callback, handler);
      }
  
      /**
       * Cancels the current profile download session.
       *
       * @param transactionId The transaction ID returned by SM-DP+ server.
       * @param callback The callback to get the result, which represents an {@code
       *     CancelSessionResponse} defined in GSMA RSP v2.0+.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void cancelSession(byte[] transactionId, @EuiccCardManager.CancelReason int reason,
              AsyncResultCallback<byte[]> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_CANCEL_SESSION)
                                  .addChildAsBytes(Tags.TAG_CTX_0, transactionId)
                                  .addChildAsInteger(Tags.TAG_CTX_1, reason)
                                  .build().toHex())),
                  (byte[] response) ->
                          parseResponseAndCheckSimpleError(response,
                                  EuiccCardErrorException.OPERATION_CANCEL_SESSION).toBytes(),
                  callback, handler);
      }
  
      /**
       * Lists all notifications of the given {@code notificationEvents}.
       *
       * @param events Bits of the event types ({@link EuiccNotification.Event}) to list.
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void listNotifications(@EuiccNotification.Event int events,
              AsyncResultCallback<EuiccNotification[]> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_LIST_NOTIFICATION)
                                  .addChildAsBits(Tags.TAG_CTX_1, events)
                                  .build().toHex())),
                  response -> {
                      Asn1Node root = parseResponseAndCheckSimpleError(response,
                              EuiccCardErrorException.OPERATION_LIST_NOTIFICATIONS);
                      List<Asn1Node> nodes = root.getChild(Tags.TAG_CTX_COMP_0).getChildren();
                      EuiccNotification[] notifications = new EuiccNotification[nodes.size()];
                      for (int i = 0; i < notifications.length; ++i) {
                          notifications[i] = createNotification(nodes.get(i));
                      }
                      return notifications;
                  },
                  callback, handler);
      }
  
      /**
       * Retrieves contents of all notification of the given {@code events}.
       *
       * @param events Bits of the event types ({@link EuiccNotification.Event}) to list.
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void retrieveNotificationList(@EuiccNotification.Event int events,
              AsyncResultCallback<EuiccNotification[]> callback, Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(
                                  Asn1Node.newBuilder(Tags.TAG_RETRIEVE_NOTIFICATIONS_LIST)
                                          .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
                                                  .addChildAsBits(Tags.TAG_CTX_1, events))
                                          .build().toHex())),
                  response -> {
                      Asn1Node root = parseResponse(response);
                      if (root.hasChild(Tags.TAG_CTX_1)) {
                          // SGP.22 v2.0 RetrieveNotificationsListResponse
                          int error = root.getChild(Tags.TAG_CTX_1).asInteger();
                          switch (error) {
                              case CODE_NO_RESULT_AVAILABLE:
                                  return new EuiccNotification[0];
                              default:
                                  throw new EuiccCardErrorException(
                                          EuiccCardErrorException.OPERATION_RETRIEVE_NOTIFICATION,
                                          error);
                          }
                      }
                      List<Asn1Node> nodes = root.getChild(Tags.TAG_CTX_COMP_0).getChildren();
                      EuiccNotification[] notifications = new EuiccNotification[nodes.size()];
                      for (int i = 0; i < notifications.length; ++i) {
                          notifications[i] = createNotification(nodes.get(i));
                      }
                      return notifications;
                  },
                  callback, handler);
      }
  
      /**
       * Retrieves the content of a notification of the given {@code seqNumber}.
       *
       * @param seqNumber The sequence number of the notification.
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void retrieveNotification(int seqNumber, AsyncResultCallback<EuiccNotification> callback,
              Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(
                                  Asn1Node.newBuilder(Tags.TAG_RETRIEVE_NOTIFICATIONS_LIST)
                                          .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
                                                  .addChildAsInteger(Tags.TAG_CTX_0, seqNumber))
                                          .build().toHex())),
                  response -> {
                      Asn1Node root = parseResponseAndCheckSimpleError(response,
                              EuiccCardErrorException.OPERATION_RETRIEVE_NOTIFICATION);
                      List<Asn1Node> nodes = root.getChild(Tags.TAG_CTX_COMP_0).getChildren();
                      if (nodes.size() > 0) {
                          return createNotification(nodes.get(0));
                      }
                      return null;
                  },
                  callback, handler);
      }
  
      /**
       * Removes a notification from eUICC.
       *
       * @param seqNumber The sequence number of the notification.
       * @param callback The callback to get the result.
       * @param handler The handler to run the callback.
       * @since 2.0.0 [GSMA SGP.22]
       */
      public void removeNotificationFromList(int seqNumber, AsyncResultCallback<Void> callback,
              Handler handler) {
          sendApdu(
                  newRequestProvider((RequestBuilder requestBuilder) ->
                          requestBuilder.addStoreData(
                                  Asn1Node.newBuilder(Tags.TAG_REMOVE_NOTIFICATION_FROM_LIST)
                                          .addChildAsInteger(Tags.TAG_CTX_0, seqNumber)
                                          .build().toHex())),
                  response -> {
                      // SGP.22 v2.0 NotificationSentResponse
                      int result = parseSimpleResult(response);
                      if (result != CODE_OK && result != CODE_NOTHING_TO_DELETE) {
                          throw new EuiccCardErrorException(
                                  EuiccCardErrorException.OPERATION_REMOVE_NOTIFICATION_FROM_LIST,
                                  result);
                      }
                      return null;
                  },
                  callback, handler);
      }
  
      /**
       * Sets a device capability version as the child of the given device capability ASN1 node
       * builder.
       *
       * @param devCapBuilder The ASN1 node builder to modify.
       * @param devCapItem The device capability and its supported version in pair.
       */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
      public void addDeviceCapability(Asn1Node.Builder devCapBuilder, String devCapItem) {
          String[] split = devCapItem.split(",");
          if (split.length != 2) {
              loge("Invalid device capability item: " + Arrays.toString(split));
              return;
          }
  
          String devCap = split[0].trim();
          Integer version;
          try {
              version = Integer.parseInt(split[1].trim());
          } catch (NumberFormatException e) {
              loge("Invalid device capability version number.", e);
              return;
          }
  
          byte[] versionBytes = new byte[] { version.byteValue(), 0, 0 };
          switch (devCap) {
              case DEV_CAP_GSM:
                  devCapBuilder.addChildAsBytes(Tags.TAG_CTX_0, versionBytes);
                  break;
              case DEV_CAP_UTRAN:
                  devCapBuilder.addChildAsBytes(Tags.TAG_CTX_1, versionBytes);
                  break;
              case DEV_CAP_CDMA_1X:
                  devCapBuilder.addChildAsBytes(Tags.TAG_CTX_2, versionBytes);
                  break;
              case DEV_CAP_HRPD:
                  devCapBuilder.addChildAsBytes(Tags.TAG_CTX_3, versionBytes);
                  break;
              case DEV_CAP_EHRPD:
                  devCapBuilder.addChildAsBytes(Tags.TAG_CTX_4, versionBytes);
                  break;
              case DEV_CAP_EUTRAN:
                  devCapBuilder.addChildAsBytes(Tags.TAG_CTX_5, versionBytes);
                  break;
              case DEV_CAP_NFC:
                  devCapBuilder.addChildAsBytes(Tags.TAG_CTX_6, versionBytes);
                  break;
              case DEV_CAP_CRL:
                  devCapBuilder.addChildAsBytes(Tags.TAG_CTX_7, versionBytes);
                  break;
              default:
                  loge("Invalid device capability name: " + devCap);
                  break;
          }
      }
  
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
      protected byte[] getDeviceId() {
          Phone phone = PhoneFactory.getPhone(getPhoneId());
          if (phone == null) {
              return new byte[8];
          }
          return getDeviceId(phone.getDeviceId(), mSpecVersion);
      }
  
      /**
       * Different versions of SGP.22 specify different encodings of the device's IMEI, so we handle
       * those differences here.
       *
       * @param imei The IMEI of the device. Assumed to be 15 decimal digits.
       * @param specVersion The SGP.22 version which we're encoding the IMEI for.
       * @return A byte string representing the given IMEI according to the specified SGP.22 version.
       */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
      public static byte[] getDeviceId(String imei, EuiccSpecVersion specVersion) {
          byte[] imeiBytes = new byte[8];
          // The IMEI's encoding is version-dependent.
          if (specVersion.compareTo(SGP22_V_2_1) >= 0) {
              /*
               * In SGP.22 v2.1, a clarification was added to clause 4.2 that requires the nibbles of
               * the last byte to be swapped from normal TBCD encoding (so put back in normal order):
               *
               * The IMEI (including the check digit) SHALL be represented as a string of 8 octets
               * that is coded as a Telephony Binary Coded Decimal String as defined in 3GPP TS 29.002
               * [63], except that the last octet contains the check digit (in high nibble) and an 'F'
               * filler (in low nibble). It SHOULD be present if the Device contains a non-removable
               * eUICC.
               *
               * 3GPP TS 29.002 clause 17.7.8 in turn says this:
               *
               * TBCD-STRING ::= OCTET STRING
               * This type (Telephony Binary Coded Decimal String) is used to represent several digits
               * from 0 through 9, *, #, a, b, c, two digits per octet, each digit encoded 0000 to
               * 1001 (0 to 9), 1010 (*), 1011 (#), 1100 (a), 1101 (b) or 1110 (c); 1111 used as
               * filler when there is an odd number of digits.
               * Bits 8765 of octet n encoding digit 2n
               * Bits 4321 of octet n encoding digit 2(n-1) + 1
               */
              // Since the IMEI is always just decimal digits, we can still use BCD encoding (which
              // correctly swaps digit ordering within bytes), but we have to manually pad a 0xF value
              // instead of 0.
              imei += 'F';
              IccUtils.bcdToBytes(imei, imeiBytes);
              // And now the funky last byte flip (this is not normal TBCD, the GSMA added it on top
              // just for the IMEI for some reason). Bitwise operations promote to int first, so we
              // have to do some extra masking.
              byte last = imeiBytes[7];
              imeiBytes[7] = (byte) ((last & 0xFF) << 4 | ((last & 0xFF) >>> 4));
          } else {
              /*
               * Prior to SGP.22 v2.1, clause 4.2 reads as follows:
               *
               * The IMEI (including the check digit) SHALL be represented as a string of 8 octets
               * that is BCD coded as defined in 3GPP TS 23.003 [35]. It SHOULD be present if the
               * Device contains a non-removable eUICC.
               *
               * It appears that 3GPP TS 23.003 doesn't define anything about BCD encoding, it just
               * defines what IMEI and a few other telephony identifiers are. We default to normal BCD
               * encoding since the spec is unclear here.
               */
              IccUtils.bcdToBytes(imei, imeiBytes);
          }
          return imeiBytes;
      }
  
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
      protected Resources getResources()  {
          return Resources.getSystem();
      }
  
      private RequestProvider newRequestProvider(ApduRequestBuilder builder) {
          return (selectResponse, requestBuilder) -> {
              EuiccSpecVersion ver = getOrExtractSpecVersion(selectResponse);
              if (ver == null) {
                  throw new EuiccCardException("Cannot get eUICC spec version.");
              }
              try {
                  if (ver.compareTo(SGP22_V_2_0) < 0) {
                      throw new EuiccCardException("eUICC spec version is unsupported: " + ver);
                  }
                  builder.build(requestBuilder);
              } catch (InvalidAsn1DataException | TagNotFoundException e) {
                  throw new EuiccCardException("Cannot parse ASN1 to build request.", e);
              }
          };
      }
  
      private EuiccSpecVersion getOrExtractSpecVersion(byte[] selectResponse) {
          // Uses the cached version.
          if (mSpecVersion != null) {
              return mSpecVersion;
          }
          // Parses and caches the version.
          EuiccSpecVersion ver = EuiccSpecVersion.fromOpenChannelResponse(selectResponse);
          if (ver != null) {
              synchronized (mLock) {
                  if (mSpecVersion == null) {
                      mSpecVersion = ver;
                  }
              }
          }
          return ver;
      }
  
      /**
       * A wrapper on {@link ApduSender#send(RequestProvider, ApduSenderResultCallback, Handler)} to
       * leverage lambda to simplify the sending APDU code.EuiccCardErrorException.
       *
       * @param requestBuilder Builds the request of APDU commands.
       * @param responseHandler Converts the APDU response from bytes to expected result.
       * @param <T> Type of the originally expected result.
       */
      private <T> void sendApdu(RequestProvider requestBuilder,
              ApduResponseHandler<T> responseHandler, AsyncResultCallback<T> callback,
              Handler handler) {
          sendApdu(requestBuilder, responseHandler,
                  (e) -> callback.onException(new EuiccCardException("Cannot send APDU.", e)),
                  null, callback, handler);
      }
  
      private <T> void sendApdu(RequestProvider requestBuilder,
              ApduResponseHandler<T> responseHandler,
              ApduIntermediateResultHandler intermediateResultHandler,
              AsyncResultCallback<T> callback, Handler handler) {
          sendApdu(requestBuilder, responseHandler,
                  (e) -> callback.onException(new EuiccCardException("Cannot send APDU.", e)),
                  intermediateResultHandler, callback, handler);
      }
  
      /**
       * This is a workaround solution to the bug that a SIM refresh may interrupt the modem to return
       * the reset of responses of the original APDU command. This applies to disable profile, switch
       * profile, and reset eUICC memory.
       *
       * <p>TODO: Use
       * {@link #sendApdu(RequestProvider, ApduResponseHandler, AsyncResultCallback, Handler)} when
       * this workaround is not needed.
       */
      private void sendApduWithSimResetErrorWorkaround(
              RequestProvider requestBuilder, ApduResponseHandler<Void> responseHandler,
              AsyncResultCallback<Void> callback, Handler handler) {
          sendApdu(requestBuilder, responseHandler, (e) -> {
              if (e instanceof ApduException
                      && ((ApduException) e).getApduStatus() == APDU_ERROR_SIM_REFRESH) {
                  logi("Sim is refreshed after disabling profile, no response got.");
                  callback.onResult(null);
              } else {
                  callback.onException(new EuiccCardException("Cannot send APDU.", e));
              }
          }, null, callback, handler);
      }
  
      private <T> void sendApdu(RequestProvider requestBuilder,
              ApduResponseHandler<T> responseHandler,
              ApduExceptionHandler exceptionHandler,
              @Nullable ApduIntermediateResultHandler intermediateResultHandler,
              AsyncResultCallback<T> callback,
              Handler handler) {
          mApduSender.send(requestBuilder, new ApduSenderResultCallback() {
              @Override
              public void onResult(byte[] response) {
                  try {
                      callback.onResult(responseHandler.handleResult(response));
                  } catch (EuiccCardException e) {
                      callback.onException(e);
                  } catch (InvalidAsn1DataException | TagNotFoundException e) {
                      callback.onException(new EuiccCardException(
                              "Cannot parse response: " + IccUtils.bytesToHexString(response), e));
                  }
              }
  
              @Override
              public boolean shouldContinueOnIntermediateResult(IccIoResult result) {
                  if (intermediateResultHandler == null) {
                      return true;
                  }
                  return intermediateResultHandler.shouldContinue(result);
              }
  
              @Override
              public void onException(Throwable e) {
                  exceptionHandler.handleException(e);
              }
          }, handler);
      }
  
      private static void buildProfile(Asn1Node profileNode, EuiccProfileInfo.Builder profileBuilder)
              throws TagNotFoundException, InvalidAsn1DataException {
          if (profileNode.hasChild(Tags.TAG_NICKNAME)) {
              profileBuilder.setNickname(profileNode.getChild(Tags.TAG_NICKNAME).asString());
          }
  
          if (profileNode.hasChild(Tags.TAG_SERVICE_PROVIDER_NAME)) {
              profileBuilder.setServiceProviderName(
                      profileNode.getChild(Tags.TAG_SERVICE_PROVIDER_NAME).asString());
          }
  
          if (profileNode.hasChild(Tags.TAG_PROFILE_NAME)) {
              profileBuilder.setProfileName(
                      profileNode.getChild(Tags.TAG_PROFILE_NAME).asString());
          }
  
          if (profileNode.hasChild(Tags.TAG_OPERATOR_ID)) {
              profileBuilder.setCarrierIdentifier(
                      buildCarrierIdentifier(profileNode.getChild(Tags.TAG_OPERATOR_ID)));
          }
  
          if (profileNode.hasChild(Tags.TAG_PROFILE_STATE)) {
              // noinspection WrongConstant
              profileBuilder.setState(profileNode.getChild(Tags.TAG_PROFILE_STATE).asInteger());
          } else {
              profileBuilder.setState(EuiccProfileInfo.PROFILE_STATE_DISABLED);
          }
  
          if (profileNode.hasChild(Tags.TAG_PROFILE_CLASS)) {
              // noinspection WrongConstant
              profileBuilder.setProfileClass(
                      profileNode.getChild(Tags.TAG_PROFILE_CLASS).asInteger());
          } else {
              profileBuilder.setProfileClass(EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL);
          }
  
          if (profileNode.hasChild(Tags.TAG_PROFILE_POLICY_RULE)) {
              // noinspection WrongConstant
              profileBuilder.setPolicyRules(
                      profileNode.getChild(Tags.TAG_PROFILE_POLICY_RULE).asBits());
          }
  
          if (profileNode.hasChild(Tags.TAG_CARRIER_PRIVILEGE_RULES)) {
              List<Asn1Node> refArDoNodes = profileNode.getChild(Tags.TAG_CARRIER_PRIVILEGE_RULES)
                      .getChildren(Tags.TAG_REF_AR_DO);
              UiccAccessRule[] rules = buildUiccAccessRule(refArDoNodes);
              List<UiccAccessRule> rulesList = null;
              if (rules != null) {
                  rulesList = Arrays.asList(rules);
              }
              profileBuilder.setUiccAccessRule(rulesList);
          }
      }
  
      private static CarrierIdentifier buildCarrierIdentifier(Asn1Node node)
              throws InvalidAsn1DataException, TagNotFoundException {
          String gid1 = null;
          if (node.hasChild(Tags.TAG_CTX_1)) {
              gid1 = IccUtils.bytesToHexString(node.getChild(Tags.TAG_CTX_1).asBytes());
          }
          String gid2 = null;
          if (node.hasChild(Tags.TAG_CTX_2)) {
              gid2 = IccUtils.bytesToHexString(node.getChild(Tags.TAG_CTX_2).asBytes());
          }
          return new CarrierIdentifier(node.getChild(Tags.TAG_CTX_0).asBytes(), gid1, gid2);
      }
  
      @Nullable
      private static UiccAccessRule[] buildUiccAccessRule(List<Asn1Node> nodes)
              throws InvalidAsn1DataException, TagNotFoundException {
          if (nodes.isEmpty()) {
              return null;
          }
          int count = nodes.size();
          UiccAccessRule[] rules = new UiccAccessRule[count];
          for (int i = 0; i < count; i++) {
              Asn1Node node = nodes.get(i);
              Asn1Node refDoNode = node.getChild(Tags.TAG_REF_DO);
              byte[] signature = refDoNode.getChild(Tags.TAG_DEVICE_APP_ID_REF_DO).asBytes();
  
              String packageName = null;
              if (refDoNode.hasChild(Tags.TAG_PKG_REF_DO)) {
                  packageName = refDoNode.getChild(Tags.TAG_PKG_REF_DO).asString();
              }
              long accessType = 0;
              if (node.hasChild(Tags.TAG_AR_DO, Tags.TAG_PERM_AR_DO)) {
                  Asn1Node permArDoNode = node.getChild(Tags.TAG_AR_DO, Tags.TAG_PERM_AR_DO);
                  accessType = permArDoNode.asRawLong();
              }
              rules[i] = new UiccAccessRule(signature, packageName, accessType);
          }
          return rules;
      }
  
      /**
       * Creates an instance from the ASN.1 data.
       *
       * @param node This should be either {@code NotificationMetadata} or {@code PendingNotification}
       *     defined by SGP.22 v2.0.
       * @throws TagNotFoundException If no notification tag is found in the bytes.
       * @throws InvalidAsn1DataException If no valid data is found in the bytes.
       */
      private static EuiccNotification createNotification(Asn1Node node)
              throws TagNotFoundException, InvalidAsn1DataException {
          Asn1Node metadataNode;
          if (node.getTag() == Tags.TAG_NOTIFICATION_METADATA) {
              metadataNode = node;
          } else if (node.getTag() == Tags.TAG_PROFILE_INSTALLATION_RESULT) {
              metadataNode = node.getChild(Tags.TAG_PROFILE_INSTALLATION_RESULT_DATA,
                      Tags.TAG_NOTIFICATION_METADATA);
          } else {
              // Other signed notification
              metadataNode = node.getChild(Tags.TAG_NOTIFICATION_METADATA);
          }
          // noinspection WrongConstant
          return new EuiccNotification(metadataNode.getChild(Tags.TAG_SEQ).asInteger(),
                  metadataNode.getChild(Tags.TAG_TARGET_ADDR).asString(),
                  metadataNode.getChild(Tags.TAG_EVENT).asBits(),
                  node.getTag() == Tags.TAG_NOTIFICATION_METADATA ? null : node.toBytes());
      }
  
      /** Returns the first CONTEXT [0] as an integer. */
      private static int parseSimpleResult(byte[] response)
              throws EuiccCardException, TagNotFoundException, InvalidAsn1DataException {
          return parseResponse(response).getChild(Tags.TAG_CTX_0).asInteger();
      }
  
      private static Asn1Node parseResponse(byte[] response)
              throws EuiccCardException, InvalidAsn1DataException {
          Asn1Decoder decoder = new Asn1Decoder(response);
          if (!decoder.hasNextNode()) {
              throw new EuiccCardException("Empty response", null);
          }
          return decoder.nextNode();
      }
  
      /**
       * Parses the bytes into an ASN1 node and check if there is an error code represented at the
       * context 1 tag. If there is an error code, an {@link EuiccCardErrorException} will be thrown
       * with the given operation code.
       */
      private static Asn1Node parseResponseAndCheckSimpleError(byte[] response,
              @OperationCode int opCode)
              throws EuiccCardException, InvalidAsn1DataException, TagNotFoundException {
          Asn1Node root = parseResponse(response);
          if (root.hasChild(Tags.TAG_CTX_1)) {
              throw new EuiccCardErrorException(opCode, root.getChild(Tags.TAG_CTX_1).asInteger());
          }
          return root;
      }
  
      /** Strip all the trailing 'F' characters of an iccId. */
      private static String stripTrailingFs(byte[] iccId) {
          return IccUtils.stripTrailingFs(IccUtils.bchToString(iccId, 0, iccId.length));
      }
  
      /** Pad an iccId with trailing 'F' characters until the length is 20. */
      private static String padTrailingFs(String iccId) {
          if (!TextUtils.isEmpty(iccId) && iccId.length() < ICCID_LENGTH) {
              iccId += new String(new char[20 - iccId.length()]).replace('\0', 'F');
          }
          return iccId;
      }
  
      private static void loge(String message) {
          Rlog.e(LOG_TAG, message);
      }
  
      private static void loge(String message, Throwable tr) {
          Rlog.e(LOG_TAG, message, tr);
      }
  
      private static void logi(String message) {
          Rlog.i(LOG_TAG, message);
      }
  
      private static void logd(String message) {
          if (DBG) {
              Rlog.d(LOG_TAG, message);
          }
      }
  
      @Override
      public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
          super.dump(fd, pw, args);
          pw.println("EuiccCard:");
          pw.println(" mEid=" + mEid);
      }
  }
  