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

Commit f672b36e authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Support stopping sending remaining APDUs" into qt-dev

parents b7a14e51 6a1e580a
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";