blob: 0b5627382a36217f4e2a75c351d891826e1f9174 [file] [log] [blame]
/*
* 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.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 com.android.telephony.Rlog;
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";
private static final String DEV_CAP_NREPC = "nrepc";
private static final String DEV_CAP_NR5GC = "nr5gc";
private static final String DEV_CAP_EUTRAN5GC = "eutran5gc";
// 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;
case DEV_CAP_NREPC:
devCapBuilder.addChildAsBytes(Tags.TAG_CTX_9, versionBytes);
break;
case DEV_CAP_NR5GC:
devCapBuilder.addChildAsBytes(Tags.TAG_CTX_10, versionBytes);
break;
case DEV_CAP_EUTRAN5GC:
devCapBuilder.addChildAsBytes(Tags.TAG_CTX_11, 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);
}
}