Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b46e257f authored by Holly Jiuyu Sun's avatar Holly Jiuyu Sun Committed by Gerrit Code Review
Browse files

Merge "Support stopping sending remaining APDUs"

parents 81d664d1 df726dfe
Loading
Loading
Loading
Loading
+59 −22
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ 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;
@@ -47,6 +48,7 @@ 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;
@@ -109,6 +111,10 @@ public class EuiccCard extends UiccCard {
                throws EuiccCardException, TagNotFoundException, InvalidAsn1DataException;
    }

    private interface ApduIntermediateResultHandler {
        boolean shouldContinue(IccIoResult intermediateResult);
    }

    private interface ApduExceptionHandler {
        void handleException(Throwable e);
    }
@@ -230,7 +236,7 @@ public class EuiccCard extends UiccCard {
                        requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_PROFILES)
                                .addChildAsBytes(Tags.TAG_TAG_LIST, Tags.EUICC_PROFILE_TAGS)
                                .build().toHex())),
                (byte[] response) -> {
                response -> {
                    List<Asn1Node> profileNodes = new Asn1Decoder(response).nextNode()
                            .getChild(Tags.TAG_CTX_COMP_0).getChildren(Tags.TAG_PROFILE_INFO);
                    int size = profileNodes.size();
@@ -274,7 +280,7 @@ public class EuiccCard extends UiccCard {
                                    .build())
                                .addChildAsBytes(Tags.TAG_TAG_LIST, Tags.EUICC_PROFILE_TAGS)
                                .build().toHex())),
                (byte[] response) -> {
                response -> {
                    List<Asn1Node> profileNodes = new Asn1Decoder(response).nextNode()
                            .getChild(Tags.TAG_CTX_COMP_0).getChildren(Tags.TAG_PROFILE_INFO);
                    if (profileNodes.isEmpty()) {
@@ -310,7 +316,7 @@ public class EuiccCard extends UiccCard {
                            .addChildAsBoolean(Tags.TAG_CTX_1, refresh)
                            .build().toHex());
                }),
                (byte[] response) -> {
                response -> {
                    int result;
                    // SGP.22 v2.0 DisableProfileResponse
                    result = parseSimpleResult(response);
@@ -349,7 +355,7 @@ public class EuiccCard extends UiccCard {
                            .addChildAsBoolean(Tags.TAG_CTX_1, refresh)
                            .build().toHex());
                }),
                (byte[] response) -> {
                response -> {
                    int result;
                    // SGP.22 v2.0 EnableProfileResponse
                    result = parseSimpleResult(response);
@@ -393,7 +399,7 @@ public class EuiccCard extends UiccCard {
                        requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_EID)
                                .addChildAsBytes(Tags.TAG_TAG_LIST, new byte[] {Tags.TAG_EID})
                                .build().toHex())),
                (byte[] response) -> {
                response -> {
                    String eid = IccUtils.bytesToHexString(parseResponse(response)
                            .getChild(Tags.TAG_EID).asBytes());
                    synchronized (mLock) {
@@ -421,7 +427,7 @@ public class EuiccCard extends UiccCard {
                                        IccUtils.bcdToBytes(padTrailingFs(iccid)))
                                .addChildAsString(Tags.TAG_NICKNAME, nickname)
                                .build().toHex())),
                (byte[] response) -> {
                response -> {
                    // SGP.22 v2.0 SetNicknameResponse
                    int result = parseSimpleResult(response);
                    if (result != CODE_OK) {
@@ -448,7 +454,7 @@ public class EuiccCard extends UiccCard {
                            .addChildAsBytes(Tags.TAG_ICCID, iccidBytes)
                            .build().toHex());
                }),
                (byte[] response) -> {
                response -> {
                    // SGP.22 v2.0 DeleteProfileRequest
                    int result = parseSimpleResult(response);
                    if (result != CODE_OK) {
@@ -475,7 +481,7 @@ public class EuiccCard extends UiccCard {
                        requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_EUICC_MEMORY_RESET)
                                .addChildAsBits(Tags.TAG_CTX_2, options)
                                .build().toHex())),
                (byte[] response) -> {
                response -> {
                    int result = parseSimpleResult(response);
                    if (result != CODE_OK && result != CODE_NOTHING_TO_DELETE) {
                        throw new EuiccCardErrorException(
@@ -535,7 +541,7 @@ public class EuiccCard extends UiccCard {
                                Asn1Node.newBuilder(Tags.TAG_SET_DEFAULT_SMDP_ADDRESS)
                                        .addChildAsString(Tags.TAG_CTX_0, defaultSmdpAddress)
                                        .build().toHex())),
                (byte[] response) -> {
                response -> {
                    // SGP.22 v2.0 SetDefaultDpAddressResponse
                    int result = parseSimpleResult(response);
                    if (result != CODE_OK) {
@@ -560,7 +566,7 @@ public class EuiccCard extends UiccCard {
                newRequestProvider((RequestBuilder requestBuilder) ->
                        requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_RAT)
                                .build().toHex())),
                (byte[] response) -> {
                response -> {
                    Asn1Node root = parseResponse(response);
                    List<Asn1Node> nodes = root.getChildren(Tags.TAG_CTX_COMP_0);
                    EuiccRulesAuthTable.Builder builder =
@@ -682,7 +688,7 @@ public class EuiccCard extends UiccCard {
                            .addChild(ctxParams1Builder)
                            .build().toHex());
                }),
                (byte[] response) -> {
                response -> {
                    Asn1Node root = parseResponse(response);
                    if (root.hasChild(Tags.TAG_CTX_COMP_1, Tags.TAG_UNI_2)) {
                        throw new EuiccCardErrorException(
@@ -720,7 +726,7 @@ public class EuiccCard extends UiccCard {
                            builder.addChild(new Asn1Decoder(smdpCertificate).nextNode())
                                    .build().toHex());
                }),
                (byte[] response) -> {
                response -> {
                    Asn1Node root = parseResponse(response);
                    if (root.hasChild(Tags.TAG_CTX_COMP_1, Tags.TAG_UNI_2)) {
                        throw new EuiccCardErrorException(
@@ -779,7 +785,7 @@ public class EuiccCard extends UiccCard {
                        requestBuilder.addStoreData(elementSeqs.get(i).toHex());
                    }
                }),
                (byte[] response) -> {
                response -> {
                    // SGP.22 v2.0 ErrorResult
                    Asn1Node root = parseResponse(response);
                    if (root.hasChild(Tags.TAG_PROFILE_INSTALLATION_RESULT_DATA,
@@ -793,6 +799,18 @@ public class EuiccCard extends UiccCard {
                    }
                    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);
    }

@@ -834,7 +852,7 @@ public class EuiccCard extends UiccCard {
                        requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_LIST_NOTIFICATION)
                                .addChildAsBits(Tags.TAG_CTX_1, events)
                                .build().toHex())),
                (byte[] response) -> {
                response -> {
                    Asn1Node root = parseResponseAndCheckSimpleError(response,
                            EuiccCardErrorException.OPERATION_LIST_NOTIFICATIONS);
                    List<Asn1Node> nodes = root.getChild(Tags.TAG_CTX_COMP_0).getChildren();
@@ -864,7 +882,7 @@ public class EuiccCard extends UiccCard {
                                        .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
                                                .addChildAsBits(Tags.TAG_CTX_1, events))
                                        .build().toHex())),
                (byte[] response) -> {
                response -> {
                    Asn1Node root = parseResponse(response);
                    if (root.hasChild(Tags.TAG_CTX_1)) {
                        // SGP.22 v2.0 RetrieveNotificationsListResponse
@@ -905,7 +923,7 @@ public class EuiccCard extends UiccCard {
                                        .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
                                                .addChildAsInteger(Tags.TAG_CTX_0, seqNumber))
                                        .build().toHex())),
                (byte[] response) -> {
                response -> {
                    Asn1Node root = parseResponseAndCheckSimpleError(response,
                            EuiccCardErrorException.OPERATION_RETRIEVE_NOTIFICATION);
                    List<Asn1Node> nodes = root.getChild(Tags.TAG_CTX_COMP_0).getChildren();
@@ -933,7 +951,7 @@ public class EuiccCard extends UiccCard {
                                Asn1Node.newBuilder(Tags.TAG_REMOVE_NOTIFICATION_FROM_LIST)
                                        .addChildAsInteger(Tags.TAG_CTX_0, seqNumber)
                                        .build().toHex())),
                (byte[] response) -> {
                response -> {
                    // SGP.22 v2.0 NotificationSentResponse
                    int result = parseSimpleResult(response);
                    if (result != CODE_OK && result != CODE_NOTHING_TO_DELETE) {
@@ -1111,7 +1129,7 @@ public class EuiccCard extends UiccCard {
    }

    /**
     * A wrapper on {@link ApduSender#send(RequestProvider, AsyncResultCallback, Handler)} to
     * 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.
@@ -1123,7 +1141,16 @@ public class EuiccCard extends UiccCard {
            Handler handler) {
        sendApdu(requestBuilder, responseHandler,
                (e) -> callback.onException(new EuiccCardException("Cannot send APDU.", e)),
                callback, handler);
                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);
    }

    /**
@@ -1146,14 +1173,16 @@ public class EuiccCard extends UiccCard {
            } else {
                callback.onException(new EuiccCardException("Cannot send APDU.", e));
            }
        }, callback, handler);
        }, null, callback, handler);
    }

    private <T> void sendApdu(RequestProvider requestBuilder,
            ApduResponseHandler<T> responseHandler,
            ApduExceptionHandler exceptionHandler, AsyncResultCallback<T> callback,
            ApduExceptionHandler exceptionHandler,
            @Nullable ApduIntermediateResultHandler intermediateResultHandler,
            AsyncResultCallback<T> callback,
            Handler handler) {
        mApduSender.send(requestBuilder, new AsyncResultCallback<byte[]>() {
        mApduSender.send(requestBuilder, new ApduSenderResultCallback() {
            @Override
            public void onResult(byte[] response) {
                try {
@@ -1166,6 +1195,14 @@ public class EuiccCard extends UiccCard {
                }
            }

            @Override
            public boolean shouldContinueOnIntermediateResult(IccIoResult result) {
                if (intermediateResultHandler == null) {
                    return true;
                }
                return intermediateResultHandler.shouldContinue(result);
            }

            @Override
            public void onException(Throwable e) {
                exceptionHandler.handleException(e);
+14 −11
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ public class ApduSender {

    // Status code of APDU response
    private static final int STATUS_NO_ERROR = 0x9000;
    private static final int SW1_NO_ERROR = 0x91;

    private static void logv(String msg) {
        Rlog.v(LOG_TAG, msg);
@@ -89,7 +90,7 @@ public class ApduSender {
     */
    public void send(
            RequestProvider requestProvider,
            AsyncResultCallback<byte[]> resultCallback,
            ApduSenderResultCallback resultCallback,
            Handler handler) {
        synchronized (mChannelLock) {
            if (mChannelOpened) {
@@ -146,7 +147,7 @@ public class ApduSender {
    private void sendCommand(
            List<ApduCommand> commands,
            int index,
            AsyncResultCallback<byte[]> resultCallback,
            ApduSenderResultCallback resultCallback,
            Handler handler) {
        ApduCommand command = commands.get(index);
        mTransmitApdu.invoke(command, new AsyncResultCallback<IccIoResult>() {
@@ -159,23 +160,25 @@ public class ApduSender {
                            @Override
                            public void onResult(IccIoResult fullResponse) {
                                logv("Full APDU response: " + fullResponse);

                                int status = (fullResponse.sw1 << 8) | fullResponse.sw2;
                                if (status != STATUS_NO_ERROR) {
                                if (status != STATUS_NO_ERROR && fullResponse.sw1 != SW1_NO_ERROR) {
                                    closeAndReturn(command.channel, null /* response */,
                                            new ApduException(status), resultCallback, handler);
                                    return;
                                }

                                // Last command
                                if (index == commands.size() - 1) {
                                boolean continueSendCommand = index < commands.size() - 1
                                        // Checks intermediate APDU result except the last one
                                        && resultCallback.shouldContinueOnIntermediateResult(
                                                fullResponse);
                                if (continueSendCommand) {
                                    // Sends the next command
                                    sendCommand(commands, index + 1, resultCallback, handler);
                                } else {
                                    // Returns the result of the last command
                                    closeAndReturn(command.channel, fullResponse.payload,
                                            null /* exception */, resultCallback, handler);
                                    return;
                                }

                                // Sends the next command
                                sendCommand(commands, index + 1, resultCallback, handler);
                            }
                        }, handler);
            }
@@ -235,7 +238,7 @@ public class ApduSender {
            int channel,
            @Nullable byte[] response,
            @Nullable Throwable exception,
            AsyncResultCallback<byte[]> resultCallback,
            ApduSenderResultCallback resultCallback,
            Handler handler) {
        mCloseChannel.invoke(channel, new AsyncResultCallback<Boolean>() {
            @Override
+37 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.apdu;

import com.android.internal.telephony.uicc.IccIoResult;
import com.android.internal.telephony.uicc.euicc.async.AsyncResultCallback;

/**
 * Class to deliver the returned bytes from {@link ApduSender}.
 *
 * @hide
 */
public abstract class ApduSenderResultCallback extends AsyncResultCallback<byte[]> {

    /**
     * This will be called when a result to an intermediate APDU is returned. It will be called on
     * all APDU commands except the last one and executed in the {@link ApduSender}'s thread.
     *
     * @return True if next APDU command should be sent, otherwise the remaining APDU commands will
     *     be skipped.
     */
    public abstract boolean shouldContinueOnIntermediateResult(IccIoResult result);
}
+90 −1
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ import com.android.internal.telephony.uicc.IccUtils;
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.apdu.ApduException;
import com.android.internal.telephony.uicc.euicc.apdu.LogicalChannelMocker;
import com.android.internal.telephony.uicc.euicc.async.AsyncResultCallback;

@@ -724,7 +725,7 @@ public class EuiccCardTest extends TelephonyTest {
    }

    @Test
    public void testLoadBoundProfilePackage_Error() {
    public void testLoadBoundProfilePackage_ErrorAtEnd() {
        int channel = mockLogicalChannelResponses(
                // For boundProfilePackage head + initialiseSecureChannelRequest
                // (ES8+.InitialiseSecureChannel)
@@ -770,6 +771,94 @@ public class EuiccCardTest extends TelephonyTest {
        verifyStoreData(channel, "86030A0B0C"); // ES8+.LoadProfileElements
    }

    @Test
    public void testLoadBoundProfilePackage_ErrorInMiddle() {
        int channel = mockLogicalChannelResponses(
                // For boundProfilePackage head + initialiseSecureChannelRequest
                // (ES8+.InitialiseSecureChannel)
                "9000",
                // For firstSequenceOf87 (ES8+.ConfigureISDP)
                "9000",
                // For head of sequenceOf88 (ES8+.StoreMetadata)
                "9000",
                // For body (element 1) of sequenceOf88 (ES8+.StoreMetadata)
                "BF370ABF2707A205A1038101039000",
                "9000",
                // For head of sequenceOf86 (ES8+.LoadProfileElements)
                "9000",
                // For body (element 1) of sequenceOf86 (ES8+.LoadProfileElements)
                "9000",
                // Profile installation result (element 2 of sequenceOf86)
                "9000");

        ResultCaptor<byte[]> resultCaptor = new ResultCaptor<>();
        mEuiccCard.loadBoundProfilePackage(
                Asn1Node.newBuilder(0xBF36)
                        .addChild(Asn1Node.newBuilder(0xBF23))
                        .addChild(Asn1Node.newBuilder(0xA0)
                                .addChildAsBytes(0x87, new byte[] {1, 2, 3}))
                        .addChild(Asn1Node.newBuilder(0xA1)
                                .addChildAsBytes(0x88, new byte[] {4, 5, 6}))
                        .addChild(Asn1Node.newBuilder(0xA2))
                        .addChild(Asn1Node.newBuilder(0xA3)
                                .addChildAsBytes(0x86, new byte[] {7, 8, 9})
                                .addChildAsBytes(0x86, new byte[] {0xA, 0xB, 0xC}))
                        .build().toBytes(),
                resultCaptor, mHandler);
        resultCaptor.await();

        assertEquals(3, ((EuiccCardErrorException) resultCaptor.exception).getErrorCode());
        verifyStoreData(channel, "BF361FBF2300"); // ES8+.InitialiseSecureChannel
        verifyStoreData(channel, "A0058703010203"); // ES8+.ConfigureISDP
        verifyStoreData(channel, "A105"); // ES8+.StoreMetadata
        verifyStoreData(channel, "8803040506"); // ES8+.StoreMetadata
    }

    @Test
    public void testLoadBoundProfilePackage_ErrorStatus() {
        int channel = mockLogicalChannelResponses(
                // For boundProfilePackage head + initialiseSecureChannelRequest
                // (ES8+.InitialiseSecureChannel)
                "9000",
                // For firstSequenceOf87 (ES8+.ConfigureISDP)
                "9000",
                // For head of sequenceOf88 (ES8+.StoreMetadata)
                "9000",
                // For body (element 1) of sequenceOf88 (ES8+.StoreMetadata)
                "6985",
                "9000",
                // For head of sequenceOf86 (ES8+.LoadProfileElements)
                "9000",
                // For body (element 1) of sequenceOf86 (ES8+.LoadProfileElements)
                "9000",
                // Profile installation result (element 2 of sequenceOf86)
                "9000");

        ResultCaptor<byte[]> resultCaptor = new ResultCaptor<>();
        mEuiccCard.loadBoundProfilePackage(
                Asn1Node.newBuilder(0xBF36)
                        .addChild(Asn1Node.newBuilder(0xBF23))
                        .addChild(Asn1Node.newBuilder(0xA0)
                                .addChildAsBytes(0x87, new byte[] {1, 2, 3}))
                        .addChild(Asn1Node.newBuilder(0xA1)
                                .addChildAsBytes(0x88, new byte[] {4, 5, 6}))
                        .addChild(Asn1Node.newBuilder(0xA2))
                        .addChild(Asn1Node.newBuilder(0xA3)
                                .addChildAsBytes(0x86, new byte[] {7, 8, 9})
                                .addChildAsBytes(0x86, new byte[] {0xA, 0xB, 0xC}))
                        .build().toBytes(),
                resultCaptor, mHandler);
        resultCaptor.await();

        EuiccCardException e = (EuiccCardException) resultCaptor.exception;
        assertEquals(0x6985, ((ApduException) e.getCause()).getApduStatus());
        verifyStoreData(channel, "BF361FBF2300"); // ES8+.InitialiseSecureChannel
        verifyStoreData(channel, "A0058703010203"); // ES8+.ConfigureISDP
        verifyStoreData(channel, "A105"); // ES8+.StoreMetadata
        verifyStoreData(channel, "8803040506"); // ES8+.StoreMetadata
    }


    @Test
    public void testCancelSession() {
        int channel = mockLogicalChannelResponses("BF41009000");
+43 −2
Original line number Diff line number Diff line
@@ -32,8 +32,8 @@ import android.os.HandlerThread;

import com.android.internal.telephony.CommandException;
import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.uicc.IccIoResult;
import com.android.internal.telephony.uicc.IccUtils;
import com.android.internal.telephony.uicc.euicc.async.AsyncResultCallback;

import org.junit.After;
import org.junit.Before;
@@ -47,9 +47,10 @@ import java.util.concurrent.TimeUnit;
public class ApduSenderTest {
    private static final long WAIT_TIMEOUT_MLLIS = 5000;

    private static class ResponseCaptor extends AsyncResultCallback<byte[]> {
    private static class ResponseCaptor extends ApduSenderResultCallback {
        public byte[] response;
        public Throwable exception;
        public int stopApduIndex = -1;

        private final CountDownLatch mLatch = new CountDownLatch(1);

@@ -63,6 +64,18 @@ public class ApduSenderTest {
            }
        }

        @Override
        public boolean shouldContinueOnIntermediateResult(IccIoResult result) {
            if (stopApduIndex < 0) {
                return true;
            }
            if (stopApduIndex == 0) {
                return false;
            }
            stopApduIndex--;
            return true;
        }

        @Override
        public void onResult(byte[] bytes) {
            response = bytes;
@@ -185,6 +198,34 @@ public class ApduSenderTest {
                eq(0), eq(2), eq("abcd"), any());
    }

    @Test
    public void testSendMultiApdusStopEarly() throws InterruptedException {
        String aid = "B2C3D4";
        ApduSender sender = new ApduSender(mMockCi, aid, false /* supportExtendedApdu */);

        int channel = LogicalChannelMocker.mockOpenLogicalChannelResponse(mMockCi, "9000");
        LogicalChannelMocker.mockSendToLogicalChannel(mMockCi, channel, "A19000", "A29000",
                "A39000", "A49000");
        LogicalChannelMocker.mockCloseLogicalChannel(mMockCi, channel);
        mResponseCaptor.stopApduIndex = 2;

        sender.send((selectResponse, requestBuilder) -> {
            requestBuilder.addApdu(10, 1, 2, 3, 0, "a");
            requestBuilder.addApdu(10, 1, 2, 3, "ab");
            requestBuilder.addApdu(10, 1, 2, 3);
            requestBuilder.addStoreData("abcd");
        }, mResponseCaptor, mHandler);
        mResponseCaptor.await();

        assertEquals("A3", IccUtils.bytesToHexString(mResponseCaptor.response));
        verify(mMockCi).iccTransmitApduLogicalChannel(eq(channel), eq(channel | 10), eq(1), eq(2),
                eq(3), eq(0), eq("a"), any());
        verify(mMockCi).iccTransmitApduLogicalChannel(eq(channel), eq(channel | 10), eq(1), eq(2),
                eq(3), eq(1), eq("ab"), any());
        verify(mMockCi).iccTransmitApduLogicalChannel(eq(channel), eq(channel | 10), eq(1), eq(2),
                eq(3), eq(0), eq(""), any());
    }

    @Test
    public void testSendLongResponse() throws InterruptedException {
        String aid = "B2C3D4";