/** Remove whitespaces on line endings */
  private static void processLine(
      final String pLine,
      final ArmoredOutputStream pArmoredOutput,
      final PGPSignatureGenerator pSignatureGenerator)
      throws IOException, SignatureException {

    if (pLine == null) {
      return;
    }

    final char[] chars = pLine.toCharArray();
    int len = chars.length;

    while (len > 0) {
      if (!Character.isWhitespace(chars[len - 1])) {
        break;
      }
      len--;
    }

    final byte[] data = pLine.substring(0, len).getBytes("UTF-8");

    if (pArmoredOutput != null) {
      pArmoredOutput.write(data);
    }
    pSignatureGenerator.update(data);
  }
  /** Signs and/or encrypts data based on parameters of class */
  public PgpSignEncryptResult execute(
      PgpSignEncryptInputParcel input,
      CryptoInputParcel cryptoInput,
      InputData inputData,
      OutputStream outputStream) {

    int indent = 0;
    OperationLog log = new OperationLog();

    log.add(LogType.MSG_PSE, indent);
    indent += 1;

    boolean enableSignature = input.getSignatureMasterKeyId() != Constants.key.none;
    boolean enableEncryption =
        ((input.getEncryptionMasterKeyIds() != null && input.getEncryptionMasterKeyIds().length > 0)
            || input.getSymmetricPassphrase() != null);
    boolean enableCompression = (input.getCompressionId() != CompressionAlgorithmTags.UNCOMPRESSED);

    Log.d(
        Constants.TAG,
        "enableSignature:"
            + enableSignature
            + "\nenableEncryption:"
            + enableEncryption
            + "\nenableCompression:"
            + enableCompression
            + "\nenableAsciiArmorOutput:"
            + input.isEnableAsciiArmorOutput()
            + "\nisHiddenRecipients:"
            + input.isHiddenRecipients());

    // add additional key id to encryption ids (mostly to do self-encryption)
    if (enableEncryption && input.getAdditionalEncryptId() != Constants.key.none) {
      input.setEncryptionMasterKeyIds(
          Arrays.copyOf(
              input.getEncryptionMasterKeyIds(), input.getEncryptionMasterKeyIds().length + 1));
      input.getEncryptionMasterKeyIds()[input.getEncryptionMasterKeyIds().length - 1] =
          input.getAdditionalEncryptId();
    }

    ArmoredOutputStream armorOut = null;
    OutputStream out;
    if (input.isEnableAsciiArmorOutput()) {
      armorOut = new ArmoredOutputStream(new BufferedOutputStream(outputStream, 1 << 16));
      if (input.getVersionHeader() != null) {
        armorOut.setHeader("Version", input.getVersionHeader());
      }
      // if we have a charset, put it in the header
      if (input.getCharset() != null) {
        armorOut.setHeader("Charset", input.getCharset());
      }
      out = armorOut;
    } else {
      out = outputStream;
    }

    /* Get keys for signature generation for later usage */
    CanonicalizedSecretKey signingKey = null;
    if (enableSignature) {

      updateProgress(R.string.progress_extracting_signature_key, 0, 100);

      try {
        // fetch the indicated master key id (the one whose name we sign in)
        CanonicalizedSecretKeyRing signingKeyRing =
            mProviderHelper.getCanonicalizedSecretKeyRing(input.getSignatureMasterKeyId());

        // fetch the specific subkey to sign with, or just use the master key if none specified
        signingKey = signingKeyRing.getSecretKey(input.getSignatureSubKeyId());

        // Make sure we are allowed to sign here!
        if (!signingKey.canSign()) {
          log.add(LogType.MSG_PSE_ERROR_KEY_SIGN, indent);
          return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
        }

        switch (signingKey.getSecretKeyType()) {
          case DIVERT_TO_CARD:
          case PASSPHRASE_EMPTY:
            {
              if (!signingKey.unlock(new Passphrase())) {
                throw new AssertionError(
                    "PASSPHRASE_EMPTY/DIVERT_TO_CARD keyphrase not unlocked with empty passphrase."
                        + " This is a programming error!");
              }
              break;
            }

          case PIN:
          case PATTERN:
          case PASSPHRASE:
            {
              Passphrase localPassphrase = cryptoInput.getPassphrase();
              if (localPassphrase == null) {
                try {
                  localPassphrase =
                      getCachedPassphrase(signingKeyRing.getMasterKeyId(), signingKey.getKeyId());
                } catch (PassphraseCacheInterface.NoSecretKeyException ignored) {
                }
              }
              if (localPassphrase == null) {
                log.add(LogType.MSG_PSE_PENDING_PASSPHRASE, indent + 1);
                return new PgpSignEncryptResult(
                    log,
                    RequiredInputParcel.createRequiredSignPassphrase(
                        signingKeyRing.getMasterKeyId(),
                        signingKey.getKeyId(),
                        cryptoInput.getSignatureTime()),
                    cryptoInput);
              }
              if (!signingKey.unlock(localPassphrase)) {
                log.add(LogType.MSG_PSE_ERROR_BAD_PASSPHRASE, indent);
                return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
              }
              break;
            }

          case GNU_DUMMY:
            {
              log.add(LogType.MSG_PSE_ERROR_UNLOCK, indent);
              return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
            }
          default:
            {
              throw new AssertionError("Unhandled SecretKeyType! (should not happen)");
            }
        }

      } catch (ProviderHelper.NotFoundException e) {
        log.add(LogType.MSG_PSE_ERROR_SIGN_KEY, indent);
        return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
      } catch (PgpGeneralException e) {
        log.add(LogType.MSG_PSE_ERROR_UNLOCK, indent);
        return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
      }

      // Use preferred hash algo
      int requestedAlgorithm = input.getSignatureHashAlgorithm();
      ArrayList<Integer> supported = signingKey.getSupportedHashAlgorithms();
      if (requestedAlgorithm == PgpConstants.OpenKeychainHashAlgorithmTags.USE_PREFERRED) {
        // get most preferred
        input.setSignatureHashAlgorithm(supported.get(0));
      } else if (!supported.contains(requestedAlgorithm)) {
        log.add(LogType.MSG_PSE_ERROR_HASH_ALGO, indent);
        return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
      }
    }
    updateProgress(R.string.progress_preparing_streams, 2, 100);

    /* Initialize PGPEncryptedDataGenerator for later usage */
    PGPEncryptedDataGenerator cPk = null;
    if (enableEncryption) {

      // Use preferred encryption algo
      int algo = input.getSymmetricEncryptionAlgorithm();
      if (algo == PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED) {
        // get most preferred
        // TODO: get from recipients
        algo = PgpConstants.sPreferredSymmetricAlgorithms.get(0);
      }
      // has Integrity packet enabled!
      JcePGPDataEncryptorBuilder encryptorBuilder =
          new JcePGPDataEncryptorBuilder(algo)
              .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME)
              .setWithIntegrityPacket(true);

      cPk = new PGPEncryptedDataGenerator(encryptorBuilder);

      if (input.getSymmetricPassphrase() != null) {
        // Symmetric encryption
        log.add(LogType.MSG_PSE_SYMMETRIC, indent);

        JcePBEKeyEncryptionMethodGenerator symmetricEncryptionGenerator =
            new JcePBEKeyEncryptionMethodGenerator(input.getSymmetricPassphrase().getCharArray());
        cPk.addMethod(symmetricEncryptionGenerator);
      } else {
        log.add(LogType.MSG_PSE_ASYMMETRIC, indent);

        // Asymmetric encryption
        for (long id : input.getEncryptionMasterKeyIds()) {
          try {
            CanonicalizedPublicKeyRing keyRing =
                mProviderHelper.getCanonicalizedPublicKeyRing(KeyRings.buildUnifiedKeyRingUri(id));
            Set<Long> encryptSubKeyIds = keyRing.getEncryptIds();
            for (Long subKeyId : encryptSubKeyIds) {
              CanonicalizedPublicKey key = keyRing.getPublicKey(subKeyId);
              cPk.addMethod(key.getPubKeyEncryptionGenerator(input.isHiddenRecipients()));
              log.add(
                  LogType.MSG_PSE_KEY_OK,
                  indent + 1,
                  KeyFormattingUtils.convertKeyIdToHex(subKeyId));
            }
            if (encryptSubKeyIds.isEmpty()) {
              log.add(
                  LogType.MSG_PSE_KEY_WARN, indent + 1, KeyFormattingUtils.convertKeyIdToHex(id));
              if (input.isFailOnMissingEncryptionKeyIds()) {
                return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
              }
            }
          } catch (ProviderHelper.NotFoundException e) {
            log.add(
                LogType.MSG_PSE_KEY_UNKNOWN, indent + 1, KeyFormattingUtils.convertKeyIdToHex(id));
            if (input.isFailOnMissingEncryptionKeyIds()) {
              return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
            }
          }
        }
      }
    }

    /* Initialize signature generator object for later usage */
    PGPSignatureGenerator signatureGenerator = null;
    if (enableSignature) {
      updateProgress(R.string.progress_preparing_signature, 4, 100);

      try {
        boolean cleartext =
            input.isCleartextSignature() && input.isEnableAsciiArmorOutput() && !enableEncryption;
        signatureGenerator =
            signingKey.getDataSignatureGenerator(
                input.getSignatureHashAlgorithm(),
                cleartext,
                cryptoInput.getCryptoData(),
                cryptoInput.getSignatureTime());
      } catch (PgpGeneralException e) {
        log.add(LogType.MSG_PSE_ERROR_NFC, indent);
        return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
      }
    }

    ProgressScaler progressScaler = new ProgressScaler(mProgressable, 8, 95, 100);
    PGPCompressedDataGenerator compressGen = null;
    OutputStream pOut;
    OutputStream encryptionOut = null;
    BCPGOutputStream bcpgOut;

    ByteArrayOutputStream detachedByteOut = null;
    ArmoredOutputStream detachedArmorOut = null;
    BCPGOutputStream detachedBcpgOut = null;

    try {

      if (enableEncryption) {
        /* actual encryption */
        updateProgress(R.string.progress_encrypting, 8, 100);
        log.add(enableSignature ? LogType.MSG_PSE_SIGCRYPTING : LogType.MSG_PSE_ENCRYPTING, indent);
        indent += 1;

        encryptionOut = cPk.open(out, new byte[1 << 16]);

        if (enableCompression) {
          log.add(LogType.MSG_PSE_COMPRESSING, indent);
          compressGen = new PGPCompressedDataGenerator(input.getCompressionId());
          bcpgOut = new BCPGOutputStream(compressGen.open(encryptionOut));
        } else {
          bcpgOut = new BCPGOutputStream(encryptionOut);
        }

        if (enableSignature) {
          signatureGenerator.generateOnePassVersion(false).encode(bcpgOut);
        }

        PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator();
        char literalDataFormatTag;
        if (input.isCleartextSignature()) {
          literalDataFormatTag = PGPLiteralData.UTF8;
        } else {
          literalDataFormatTag = PGPLiteralData.BINARY;
        }
        pOut =
            literalGen.open(
                bcpgOut,
                literalDataFormatTag,
                inputData.getOriginalFilename(),
                new Date(),
                new byte[1 << 16]);

        long alreadyWritten = 0;
        int length;
        byte[] buffer = new byte[1 << 16];
        InputStream in = inputData.getInputStream();
        while ((length = in.read(buffer)) > 0) {
          pOut.write(buffer, 0, length);

          // update signature buffer if signature is requested
          if (enableSignature) {
            signatureGenerator.update(buffer, 0, length);
          }

          alreadyWritten += length;
          if (inputData.getSize() > 0) {
            long progress = 100 * alreadyWritten / inputData.getSize();
            progressScaler.setProgress((int) progress, 100);
          }
        }

        literalGen.close();
        indent -= 1;

      } else if (enableSignature
          && input.isCleartextSignature()
          && input.isEnableAsciiArmorOutput()) {
        /* cleartext signature: sign-only of ascii text */

        updateProgress(R.string.progress_signing, 8, 100);
        log.add(LogType.MSG_PSE_SIGNING_CLEARTEXT, indent);

        // write -----BEGIN PGP SIGNED MESSAGE-----
        armorOut.beginClearText(input.getSignatureHashAlgorithm());

        InputStream in = inputData.getInputStream();
        final BufferedReader reader = new BufferedReader(new InputStreamReader(in));

        // update signature buffer with first line
        processLine(reader.readLine(), armorOut, signatureGenerator);

        // TODO: progress: fake annealing?
        while (true) {
          String line = reader.readLine();

          // end cleartext signature with newline, see http://tools.ietf.org/html/rfc4880#section-7
          if (line == null) {
            armorOut.write(NEW_LINE);
            break;
          }

          armorOut.write(NEW_LINE);

          // update signature buffer with input line
          signatureGenerator.update(NEW_LINE);
          processLine(line, armorOut, signatureGenerator);
        }

        armorOut.endClearText();

        pOut = new BCPGOutputStream(armorOut);
      } else if (enableSignature && input.isDetachedSignature()) {
        /* detached signature */

        updateProgress(R.string.progress_signing, 8, 100);
        log.add(LogType.MSG_PSE_SIGNING_DETACHED, indent);

        InputStream in = inputData.getInputStream();

        // handle output stream separately for detached signatures
        detachedByteOut = new ByteArrayOutputStream();
        OutputStream detachedOut = detachedByteOut;
        if (input.isEnableAsciiArmorOutput()) {
          detachedArmorOut =
              new ArmoredOutputStream(new BufferedOutputStream(detachedOut, 1 << 16));
          if (input.getVersionHeader() != null) {
            detachedArmorOut.setHeader("Version", input.getVersionHeader());
          }

          detachedOut = detachedArmorOut;
        }
        detachedBcpgOut = new BCPGOutputStream(detachedOut);

        long alreadyWritten = 0;
        int length;
        byte[] buffer = new byte[1 << 16];
        while ((length = in.read(buffer)) > 0) {
          // no output stream is written, no changed to original data!

          signatureGenerator.update(buffer, 0, length);

          alreadyWritten += length;
          if (inputData.getSize() > 0) {
            long progress = 100 * alreadyWritten / inputData.getSize();
            progressScaler.setProgress((int) progress, 100);
          }
        }

        pOut = null;
      } else if (enableSignature && !input.isCleartextSignature() && !input.isDetachedSignature()) {
        /* sign-only binary (files/data stream) */

        updateProgress(R.string.progress_signing, 8, 100);
        log.add(LogType.MSG_PSE_SIGNING, indent);

        InputStream in = inputData.getInputStream();

        if (enableCompression) {
          compressGen = new PGPCompressedDataGenerator(input.getCompressionId());
          bcpgOut = new BCPGOutputStream(compressGen.open(out));
        } else {
          bcpgOut = new BCPGOutputStream(out);
        }

        signatureGenerator.generateOnePassVersion(false).encode(bcpgOut);

        PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator();
        pOut =
            literalGen.open(
                bcpgOut,
                PGPLiteralData.BINARY,
                inputData.getOriginalFilename(),
                new Date(),
                new byte[1 << 16]);

        long alreadyWritten = 0;
        int length;
        byte[] buffer = new byte[1 << 16];
        while ((length = in.read(buffer)) > 0) {
          pOut.write(buffer, 0, length);

          signatureGenerator.update(buffer, 0, length);

          alreadyWritten += length;
          if (inputData.getSize() > 0) {
            long progress = 100 * alreadyWritten / inputData.getSize();
            progressScaler.setProgress((int) progress, 100);
          }
        }

        literalGen.close();
      } else {
        pOut = null;
        // TODO: Is this log right?
        log.add(LogType.MSG_PSE_CLEARSIGN_ONLY, indent);
      }

      if (enableSignature) {
        updateProgress(R.string.progress_generating_signature, 95, 100);
        try {
          if (detachedBcpgOut != null) {
            signatureGenerator.generate().encode(detachedBcpgOut);
          } else {
            signatureGenerator.generate().encode(pOut);
          }
        } catch (NfcSyncPGPContentSignerBuilder.NfcInteractionNeeded e) {
          // this secret key diverts to a OpenPGP card, throw exception with hash that will be
          // signed
          log.add(LogType.MSG_PSE_PENDING_NFC, indent);
          return new PgpSignEncryptResult(
              log,
              RequiredInputParcel.createNfcSignOperation(
                  signingKey.getRing().getMasterKeyId(),
                  signingKey.getKeyId(),
                  e.hashToSign,
                  e.hashAlgo,
                  cryptoInput.getSignatureTime()),
              cryptoInput);
        }
      }

      // closing outputs
      // NOTE: closing needs to be done in the correct order!
      if (encryptionOut != null) {
        if (compressGen != null) {
          compressGen.close();
        }

        encryptionOut.close();
      }
      // Note: Closing ArmoredOutputStream does not close the underlying stream
      if (armorOut != null) {
        armorOut.close();
      }
      // Note: Closing ArmoredOutputStream does not close the underlying stream
      if (detachedArmorOut != null) {
        detachedArmorOut.close();
      }
      // Also closes detachedBcpgOut
      if (detachedByteOut != null) {
        detachedByteOut.close();
      }
      if (out != null) {
        out.close();
      }
      if (outputStream != null) {
        outputStream.close();
      }

    } catch (SignatureException e) {
      log.add(LogType.MSG_PSE_ERROR_SIG, indent);
      return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
    } catch (PGPException e) {
      log.add(LogType.MSG_PSE_ERROR_PGP, indent);
      return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
    } catch (IOException e) {
      log.add(LogType.MSG_PSE_ERROR_IO, indent);
      return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
    }

    updateProgress(R.string.progress_done, 100, 100);

    log.add(LogType.MSG_PSE_OK, indent);
    PgpSignEncryptResult result = new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_OK, log);
    if (detachedByteOut != null) {
      try {
        detachedByteOut.flush();
        detachedByteOut.close();
      } catch (IOException e) {
        // silently catch
      }
      result.setDetachedSignature(detachedByteOut.toByteArray());
    }
    return result;
  }