content: /*
   * Copyright (C) 2018 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.internal.telephony.uicc.euicc.apdu;
  
  import android.annotation.Nullable;
  import android.os.Handler;
  import android.telephony.IccOpenLogicalChannelResponse;
  import android.telephony.Rlog;
  
  import com.android.internal.telephony.CommandsInterface;
  import com.android.internal.telephony.uicc.IccIoResult;
  import com.android.internal.telephony.uicc.euicc.async.AsyncResultCallback;
  import com.android.internal.telephony.uicc.euicc.async.AsyncResultHelper;
  
  import java.io.ByteArrayOutputStream;
  import java.io.IOException;
  import java.util.List;
  
  /**
   * This class sends a list of APDU commands to an AID on a UICC. A logical channel will be opened
   * before sending and closed after all APDU commands are sent. The complete response of the last
   * APDU command will be returned. If any APDU command returns an error status (other than
   * {@link #STATUS_NO_ERROR}) or causing an exception, an {@link ApduException} will be returned
   * immediately without sending the rest of commands. This class is thread-safe.
   *
   * @hide
   */
  public class ApduSender {
      private static final String LOG_TAG = "ApduSender";
  
      // Parameter and response used by the command to get extra responses of an APDU command.
      private static final int INS_GET_MORE_RESPONSE = 0xC0;
      private static final int SW1_MORE_RESPONSE = 0x61;
  
      // 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);
      }
  
      private final String mAid;
      private final boolean mSupportExtendedApdu;
      private final OpenLogicalChannelInvocation mOpenChannel;
      private final CloseLogicalChannelInvocation mCloseChannel;
      private final TransmitApduLogicalChannelInvocation mTransmitApdu;
  
      // Lock for accessing mChannelOpened. We only allow to open a single logical channel at any
      // time for an AID.
      private final Object mChannelLock = new Object();
      private boolean mChannelOpened;
  
      /**
       * @param aid The AID that will be used to open a logical channel to.
       */
      public ApduSender(CommandsInterface ci, String aid, boolean supportExtendedApdu) {
          mAid = aid;
          mSupportExtendedApdu = supportExtendedApdu;
          mOpenChannel = new OpenLogicalChannelInvocation(ci);
          mCloseChannel = new CloseLogicalChannelInvocation(ci);
          mTransmitApdu = new TransmitApduLogicalChannelInvocation(ci);
      }
  
      /**
       * Sends APDU commands.
       *
       * @param requestProvider Will be called after a logical channel is opened successfully. This is
       *     in charge of building a request with all APDU commands to be sent. This won't be called
       *     if any error happens when opening a logical channel.
       * @param resultCallback Will be called after an error or the last APDU command has been
       *     executed. The result will be the full response of the last APDU command. Error will be
       *     returned as an {@link ApduException} exception.
       * @param handler The handler that {@code requestProvider} and {@code resultCallback} will be
       *     executed on.
       */
      public void send(
              RequestProvider requestProvider,
              ApduSenderResultCallback resultCallback,
              Handler handler) {
          synchronized (mChannelLock) {
              if (mChannelOpened) {
                  AsyncResultHelper.throwException(
                          new ApduException("Logical channel has already been opened."),
                          resultCallback, handler);
                  return;
              }
              mChannelOpened = true;
          }
  
          mOpenChannel.invoke(mAid, new AsyncResultCallback<IccOpenLogicalChannelResponse>() {
              @Override
              public void onResult(IccOpenLogicalChannelResponse openChannelResponse) {
                  int channel = openChannelResponse.getChannel();
                  int status = openChannelResponse.getStatus();
                  if (channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL
                          || status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR) {
                      synchronized (mChannelLock) {
                          mChannelOpened = false;
                      }
                      resultCallback.onException(
                              new ApduException("Failed to open logical channel opened for AID: "
                                      + mAid + ", with status: " + status));
                      return;
                  }
  
                  RequestBuilder builder = new RequestBuilder(channel, mSupportExtendedApdu);
                  Throwable requestException = null;
                  try {
                      requestProvider.buildRequest(openChannelResponse.getSelectResponse(), builder);
                  } catch (Throwable e) {
                      requestException = e;
                  }
                  if (builder.getCommands().isEmpty() || requestException != null) {
                      // Just close the channel if we don't have commands to send or an error
                      // was encountered.
                      closeAndReturn(channel, null /* response */, requestException, resultCallback,
                              handler);
                      return;
                  }
                  sendCommand(builder.getCommands(), 0 /* index */, resultCallback, handler);
              }
          }, handler);
      }
  
      /**
       * Sends the current command and then continue to send the next one. If this is the last
       * command or any error happens, {@code resultCallback} will be called.
       *
       * @param commands All commands to be sent.
       * @param index The current command index.
       */
      private void sendCommand(
              List<ApduCommand> commands,
              int index,
              ApduSenderResultCallback resultCallback,
              Handler handler) {
          ApduCommand command = commands.get(index);
          mTransmitApdu.invoke(command, new AsyncResultCallback<IccIoResult>() {
              @Override
              public void onResult(IccIoResult response) {
                  // A long response may need to be fetched by multiple following-up APDU
                  // commands. Makes sure that we get the complete response.
                  getCompleteResponse(command.channel, response, null /* responseBuilder */,
                          new AsyncResultCallback<IccIoResult>() {
                              @Override
                              public void onResult(IccIoResult fullResponse) {
                                  logv("Full APDU response: " + fullResponse);
                                  int status = (fullResponse.sw1 << 8) | fullResponse.sw2;
                                  if (status != STATUS_NO_ERROR && fullResponse.sw1 != SW1_NO_ERROR) {
                                      closeAndReturn(command.channel, null /* response */,
                                              new ApduException(status), resultCallback, handler);
                                      return;
                                  }
  
                                  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);
                                  }
                              }
                          }, handler);
              }
          }, handler);
      }
  
      /**
       * Gets the full response.
       *
       * @param lastResponse Will be checked to see if we need to fetch more.
       * @param responseBuilder For continuously building the full response. It should not contain the
       *     last response. If it's null, a new builder will be created.
       * @param resultCallback Error will be included in the result and no exception will be returned.
       */
      private void getCompleteResponse(
              int channel,
              IccIoResult lastResponse,
              @Nullable ByteArrayOutputStream responseBuilder,
              AsyncResultCallback<IccIoResult> resultCallback,
              Handler handler) {
          ByteArrayOutputStream resultBuilder =
                  responseBuilder == null ? new ByteArrayOutputStream() : responseBuilder;
          if (lastResponse.payload != null) {
              try {
                  resultBuilder.write(lastResponse.payload);
              } catch (IOException e) {
                  // Should never reach here.
              }
          }
          if (lastResponse.sw1 != SW1_MORE_RESPONSE) {
              lastResponse.payload = resultBuilder.toByteArray();
              resultCallback.onResult(lastResponse);
              return;
          }
  
          mTransmitApdu.invoke(
                  new ApduCommand(channel, 0 /* cls  */, INS_GET_MORE_RESPONSE, 0 /* p1 */,
                          0 /* p2 */, lastResponse.sw2, "" /* cmdHex */),
                  new AsyncResultCallback<IccIoResult>() {
                      @Override
                      public void onResult(IccIoResult response) {
                          getCompleteResponse(
                                  channel, response, resultBuilder, resultCallback, handler);
                      }
                  }, handler);
      }
  
      /**
       * Closes the opened logical channel.
       *
       * @param response If {@code exception} is null, this will be returned to {@code resultCallback}
       *     after the channel has been closed.
       * @param exception If not null, this will be returned to {@code resultCallback} after the
       *     channel has been closed.
       */
      private void closeAndReturn(
              int channel,
              @Nullable byte[] response,
              @Nullable Throwable exception,
              ApduSenderResultCallback resultCallback,
              Handler handler) {
          mCloseChannel.invoke(channel, new AsyncResultCallback<Boolean>() {
              @Override
              public void onResult(Boolean aBoolean) {
                  synchronized (mChannelLock) {
                      mChannelOpened = false;
                  }
  
                  if (exception == null) {
                      resultCallback.onResult(response);
                  } else {
                      resultCallback.onException(exception);
                  }
              }
          }, handler);
      }
  }
  