CryptoAsync: Creating MediaCodec.CryptoException

CryptoException is created with MediaCodec.CryptoInfo
Added callback onCryptoError(MediaCodec.CryptoException)
as another MediaCodec callback to signal decryption errors.

Bug: 254050543

Change-Id: Ifb8bfb44b38178ec99e6dbb607825c642bd4e46f
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index df9c539..2541a506 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -571,6 +571,10 @@
    void onError(…) {
      …
    }
+   {@literal @Override}
+   void onCryptoError(…) {
+     …
+   }
  });
  codec.configure(format, …);
  mOutputFormat = codec.getOutputFormat(); // option B
@@ -1774,6 +1778,7 @@
     private static final int CB_OUTPUT_FORMAT_CHANGE = 4;
     private static final String EOS_AND_DECODE_ONLY_ERROR_MESSAGE = "An input buffer cannot have "
             + "both BUFFER_FLAG_END_OF_STREAM and BUFFER_FLAG_DECODE_ONLY flags";
+    private static final int CB_CRYPTO_ERROR = 6;
 
     private class EventHandler extends Handler {
         private MediaCodec mCodec;
@@ -1901,6 +1906,12 @@
                     break;
                 }
 
+                case CB_CRYPTO_ERROR:
+                {
+                    mCallback.onCryptoError(mCodec, (MediaCodec.CryptoException) msg.obj);
+                    break;
+                }
+
                 case CB_OUTPUT_FORMAT_CHANGE:
                 {
                     mCallback.onOutputFormatChanged(mCodec,
@@ -2104,12 +2115,25 @@
      */
     public static final int CONFIGURE_FLAG_USE_BLOCK_MODEL = 2;
 
+    /**
+     * This flag should be used on a secure decoder only. MediaCodec configured with this
+     * flag does decryption in a separate thread. The flag requires MediaCodec to operate
+     * asynchronously and will throw CryptoException if any, in the onCryptoError()
+     * callback. Applications should override the default implementation of
+     * onCryptoError() and access the associated CryptoException.
+     *
+     * CryptoException thrown will contain {@link MediaCodec.CryptoInfo}
+     * This can be accessed using getCryptoInfo()
+     */
+    public static final int CONFIGURE_FLAG_USE_CRYPTO_ASYNC = 4;
+
     /** @hide */
     @IntDef(
         flag = true,
         value = {
             CONFIGURE_FLAG_ENCODE,
             CONFIGURE_FLAG_USE_BLOCK_MODEL,
+            CONFIGURE_FLAG_USE_CRYPTO_ASYNC,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ConfigureFlag {}
@@ -2523,19 +2547,20 @@
     public final static class CryptoException extends RuntimeException
             implements MediaDrmThrowable {
         public CryptoException(int errorCode, @Nullable String detailMessage) {
-            this(detailMessage, errorCode, 0, 0, 0);
+            this(detailMessage, errorCode, 0, 0, 0, null);
         }
 
         /**
          * @hide
          */
         public CryptoException(String message, int errorCode, int vendorError, int oemError,
-                int errorContext) {
+                int errorContext, @Nullable CryptoInfo cryptoInfo) {
             super(message);
             mErrorCode = errorCode;
             mVendorError = vendorError;
             mOemError = oemError;
             mErrorContext = errorContext;
+            mCryptoInfo = cryptoInfo;
         }
 
         /**
@@ -2654,6 +2679,16 @@
             return mErrorCode;
         }
 
+        /**
+         * Returns CryptoInfo associated with this {@link CryptoException}
+         * if any
+         *
+         * @return CryptoInfo object if any. {@link MediaCodec.CryptoException}
+         */
+        public @Nullable CryptoInfo getCryptoInfo() {
+            return mCryptoInfo;
+        }
+
         @Override
         public int getVendorError() {
             return mVendorError;
@@ -2670,6 +2705,7 @@
         }
 
         private final int mErrorCode, mVendorError, mOemError, mErrorContext;
+        private CryptoInfo mCryptoInfo;
     }
 
     /**
@@ -5088,6 +5124,25 @@
         public abstract void onError(@NonNull MediaCodec codec, @NonNull CodecException e);
 
         /**
+         * Called only when MediaCodec encountered a crypto(decryption) error when using
+         * a decoder configured with CONFIGURE_FLAG_USE_CRYPTO_ASYNC flag along with crypto
+         * or descrambler object.
+         *
+         * @param codec The MediaCodec object
+         * @param e The {@link MediaCodec.CryptoException} object with error details.
+         */
+        public void onCryptoError(@NonNull MediaCodec codec, @NonNull CryptoException e) {
+            /*
+             * A default implementation for backward compatibility.
+             * Use of CONFIGURE_FLAG_USE_CRYPTO_ASYNC requires override of this callback
+             * to receive CrytoInfo. Without an orverride an exception is thrown.
+             */
+            throw new IllegalStateException(
+                    "Client must override onCryptoError when the codec is " +
+                    "configured with CONFIGURE_FLAG_USE_CRYPTO_ASYNC.", e);
+        }
+
+        /**
          * Called when the output format has changed
          *
          * @param codec The MediaCodec object.
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index 5b0c2a2..9a4aa33 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -184,6 +184,8 @@
     jmethodID postEventFromNativeID;
     jmethodID lockAndGetContextID;
     jmethodID setAndUnlockContextID;
+    jmethodID cryptoInfoSetID;
+    jmethodID cryptoInfoSetPatternID;
     jfieldID cryptoInfoNumSubSamplesID;
     jfieldID cryptoInfoNumBytesOfClearDataID;
     jfieldID cryptoInfoNumBytesOfEncryptedDataID;
@@ -203,6 +205,7 @@
 static fields_t gFields;
 static const void *sRefBaseOwner;
 
+jint MediaErrorToJavaError(status_t err);
 
 ////////////////////////////////////////////////////////////////////////////////
 
@@ -1068,6 +1071,180 @@
     return (jthrowable)env->NewObject(clazz.get(), ctor, err, actionCode, msgObj.get());
 }
 
+static void AMessageToCryptoInfo(JNIEnv * env, const jobject & obj,
+        const sp<AMessage> & msg) {
+    if(msg == nullptr || obj == nullptr) {
+        ALOGE("CryptoAsync Nothing to do in AMessagetoCryptoInfo");
+        return;
+    }
+    size_t numSubSamples = 0;
+    sp<ABuffer> subSamplesBuffer;
+    sp<ABuffer> keyBuffer;
+    sp<ABuffer> ivBuffer;
+    CryptoPlugin::Mode mode;
+    CryptoPlugin::Pattern pattern;
+    CHECK(msg->findInt32("mode", (int*)&mode));
+    CHECK(msg->findSize("numSubSamples", &numSubSamples));
+    CHECK(msg->findBuffer("subSamples", &subSamplesBuffer));
+    CHECK(msg->findInt32("encryptBlocks", (int32_t *)&pattern.mEncryptBlocks));
+    CHECK(msg->findInt32("skipBlocks", (int32_t *)&pattern.mSkipBlocks));
+    CHECK(msg->findBuffer("iv", &ivBuffer));
+    CHECK(msg->findBuffer("key", &keyBuffer));
+
+    // subsamples
+    ScopedLocalRef<jintArray> samplesOfEncryptedDataArr(env, env->NewIntArray(numSubSamples));
+    ScopedLocalRef<jintArray> samplesOfClearDataArr(env, env->NewIntArray(numSubSamples));
+    jboolean isCopy;
+    jint *dstEncryptedSamples =
+        env->GetIntArrayElements(samplesOfEncryptedDataArr.get(), &isCopy);
+    jint * dstClearSamples =
+        env->GetIntArrayElements(samplesOfClearDataArr.get(), &isCopy);
+
+    CryptoPlugin::SubSample * samplesArray =
+        (CryptoPlugin::SubSample*)(subSamplesBuffer.get()->data());
+
+    for(int i = 0 ; i < numSubSamples ; i++) {
+        dstEncryptedSamples[i] = samplesArray[i].mNumBytesOfEncryptedData;
+        dstClearSamples[i] = samplesArray[i].mNumBytesOfClearData;
+    }
+    env->ReleaseIntArrayElements(samplesOfEncryptedDataArr.get(), dstEncryptedSamples, 0);
+    env->ReleaseIntArrayElements(samplesOfClearDataArr.get(), dstClearSamples, 0);
+    // key and iv
+    jbyteArray keyArray = NULL;
+    jbyteArray ivArray = NULL;
+    if (keyBuffer.get() != nullptr && keyBuffer->size() > 0) {
+        keyArray = env->NewByteArray(keyBuffer->size());
+        jbyte * dstKey = env->GetByteArrayElements(keyArray, &isCopy);
+        memcpy(dstKey, keyBuffer->data(), keyBuffer->size());
+        env->ReleaseByteArrayElements(keyArray,dstKey,0);
+    }
+    if (ivBuffer.get() != nullptr && ivBuffer->size() > 0) {
+        ivArray = env->NewByteArray(ivBuffer->size());
+        jbyte *dstIv = env->GetByteArrayElements(ivArray, &isCopy);
+        memcpy(dstIv, ivBuffer->data(), ivBuffer->size());
+        env->ReleaseByteArrayElements(ivArray, dstIv,0);
+    }
+    // set samples, key and iv
+    env->CallVoidMethod(
+        obj,
+        gFields.cryptoInfoSetID,
+        (jint)numSubSamples,
+        samplesOfClearDataArr.get(),
+        samplesOfEncryptedDataArr.get(),
+        keyArray,
+        ivArray,
+        mode);
+    if (keyArray != NULL) {
+        env->DeleteLocalRef(keyArray);
+    }
+    if (ivArray != NULL) {
+        env->DeleteLocalRef(ivArray);
+    }
+    // set pattern
+    env->CallVoidMethod(
+        obj,
+        gFields.cryptoInfoSetPatternID,
+        pattern.mEncryptBlocks,
+        pattern.mSkipBlocks);
+}
+
+static void CryptoErrorToJavaError(status_t err, jint& jerr, std::string& defaultMsg) {
+    switch(err) {
+        case ERROR_DRM_NO_LICENSE:
+            jerr = gCryptoErrorCodes.cryptoErrorNoKey;
+            defaultMsg = "Crypto key not available";
+            break;
+        case ERROR_DRM_LICENSE_EXPIRED:
+            jerr = gCryptoErrorCodes.cryptoErrorKeyExpired;
+            defaultMsg = "License expired";
+            break;
+        case ERROR_DRM_RESOURCE_BUSY:
+            jerr = gCryptoErrorCodes.cryptoErrorResourceBusy;
+            defaultMsg = "Resource busy or unavailable";
+            break;
+        case ERROR_DRM_INSUFFICIENT_OUTPUT_PROTECTION:
+            jerr = gCryptoErrorCodes.cryptoErrorInsufficientOutputProtection;
+            defaultMsg = "Required output protections are not active";
+            break;
+        case ERROR_DRM_SESSION_NOT_OPENED:
+            jerr = gCryptoErrorCodes.cryptoErrorSessionNotOpened;
+            defaultMsg = "Attempted to use a closed session";
+            break;
+        case ERROR_DRM_INSUFFICIENT_SECURITY:
+            jerr = gCryptoErrorCodes.cryptoErrorInsufficientSecurity;
+            defaultMsg = "Required security level is not met";
+            break;
+        case ERROR_DRM_CANNOT_HANDLE:
+            jerr = gCryptoErrorCodes.cryptoErrorUnsupportedOperation;
+            defaultMsg = "Operation not supported in this configuration";
+            break;
+        case ERROR_DRM_FRAME_TOO_LARGE:
+            jerr = gCryptoErrorCodes.cryptoErrorFrameTooLarge;
+            defaultMsg = "Decrytped frame exceeds size of output buffer";
+            break;
+        case ERROR_DRM_SESSION_LOST_STATE:
+            jerr = gCryptoErrorCodes.cryptoErrorLostState;
+            defaultMsg = "Session state was lost, open a new session and retry";
+            break;
+        default:  // Other negative DRM error codes go out best-effort.
+            jerr = MediaErrorToJavaError(err);
+            defaultMsg = StrCryptoError(err);
+            break;
+    }
+}
+static jthrowable createCryptoException(JNIEnv *env, status_t err,
+        const char * msg = NULL, const sp<ICrypto> & crypto = NULL,
+    const sp<AMessage> & cryptoInfo = NULL) {
+    jthrowable exception = nullptr;
+    jmethodID constructID = nullptr;
+    ScopedLocalRef<jobject> cryptoInfoObject(env);
+    std::string defaultMsg = "Unknown Error";
+    jint jerr = 0;
+    // Get a class ref for CryptoException
+    ScopedLocalRef<jclass> clazz(
+        env, env->FindClass("android/media/MediaCodec$CryptoException"));
+    CHECK(clazz.get() != NULL);
+
+    // Get constructor ref for CryptoException
+    constructID = env->GetMethodID(clazz.get(), "<init>",
+            "(Ljava/lang/String;IIIILandroid/media/MediaCodec$CryptoInfo;)V");
+    CHECK(constructID != NULL);
+
+    // create detailed message for exception
+    CryptoErrorToJavaError(err, jerr, defaultMsg);
+    std::string originalMsg(msg != NULL ? msg : defaultMsg.c_str());
+    DrmStatus dStatus(err, originalMsg.c_str());
+    std::string detailedMsg(
+            DrmUtils::GetExceptionMessage(dStatus, defaultMsg.c_str(), crypto));
+    jstring msgObj = env->NewStringUTF(detailedMsg.c_str());
+
+    if (cryptoInfo != nullptr) {
+        // Class ref for CryptoInfo
+        ScopedLocalRef<jclass> clazzCryptoInfo(
+                env, env->FindClass("android/media/MediaCodec$CryptoInfo"));
+        CHECK(clazzCryptoInfo.get() != NULL);
+
+        // Constructor reference for CryptoInfo
+        jmethodID constructCryptoInfo =
+                env->GetMethodID(clazzCryptoInfo.get(), "<init>", "()V");
+        CHECK(constructCryptoInfo != NULL);
+
+        // Create CryptoInfo jobject
+        cryptoInfoObject.reset(
+                env->NewObject(clazzCryptoInfo.get(), constructCryptoInfo));
+        CHECK(cryptoInfoObject.get() != NULL);
+
+        // Translate AMesage to CryptoInfo
+        AMessageToCryptoInfo(env, cryptoInfoObject.get(), cryptoInfo);
+    }
+
+    exception = (jthrowable)env->NewObject(
+            clazz.get(), constructID, msgObj, jerr,
+            dStatus.getCdmErr(), dStatus.getOemErr(), dStatus.getContext(),
+            cryptoInfoObject.get());
+
+    return exception;
+}
 void JMediaCodec::handleCallback(const sp<AMessage> &msg) {
     int32_t arg1, arg2 = 0;
     jobject obj = NULL;
@@ -1107,6 +1284,17 @@
             break;
         }
 
+        case MediaCodec::CB_CRYPTO_ERROR:
+        {
+            int32_t err, actionCode;
+            AString errorDetail;
+            CHECK(msg->findInt32("err", &err));
+            CHECK(msg->findInt32("actionCode",&actionCode));
+            CHECK(msg->findString("errorDetail", &errorDetail));
+            obj = (jobject)createCryptoException(env, err, errorDetail.c_str(), NULL, msg);
+            break;
+        }
+
         case MediaCodec::CB_ERROR:
         {
             int32_t err, actionCode;
@@ -1144,7 +1332,6 @@
         default:
             TRESPASS();
     }
-
     env->CallVoidMethod(
             mObject,
             gFields.postEventFromNativeID,
@@ -1229,7 +1416,6 @@
     }
 }
 
-jint MediaErrorToJavaError(status_t err);
 
 }  // namespace android
 
@@ -1280,70 +1466,8 @@
 
 static void throwCryptoException(JNIEnv *env, status_t err, const char *msg,
         const sp<ICrypto> &crypto) {
-    ScopedLocalRef<jclass> clazz(
-            env, env->FindClass("android/media/MediaCodec$CryptoException"));
-    CHECK(clazz.get() != NULL);
-
-    jmethodID constructID =
-        env->GetMethodID(clazz.get(), "<init>", "(Ljava/lang/String;IIII)V");
-    CHECK(constructID != NULL);
-
-    std::string defaultMsg = "Unknown Error";
-
-    /* translate OS errors to Java API CryptoException errorCodes (which are positive) */
-    jint jerr = 0;
-    switch (err) {
-        case ERROR_DRM_NO_LICENSE:
-            jerr = gCryptoErrorCodes.cryptoErrorNoKey;
-            defaultMsg = "Crypto key not available";
-            break;
-        case ERROR_DRM_LICENSE_EXPIRED:
-            jerr = gCryptoErrorCodes.cryptoErrorKeyExpired;
-            defaultMsg = "License expired";
-            break;
-        case ERROR_DRM_RESOURCE_BUSY:
-            jerr = gCryptoErrorCodes.cryptoErrorResourceBusy;
-            defaultMsg = "Resource busy or unavailable";
-            break;
-        case ERROR_DRM_INSUFFICIENT_OUTPUT_PROTECTION:
-            jerr = gCryptoErrorCodes.cryptoErrorInsufficientOutputProtection;
-            defaultMsg = "Required output protections are not active";
-            break;
-        case ERROR_DRM_SESSION_NOT_OPENED:
-            jerr = gCryptoErrorCodes.cryptoErrorSessionNotOpened;
-            defaultMsg = "Attempted to use a closed session";
-            break;
-        case ERROR_DRM_INSUFFICIENT_SECURITY:
-            jerr = gCryptoErrorCodes.cryptoErrorInsufficientSecurity;
-            defaultMsg = "Required security level is not met";
-            break;
-        case ERROR_DRM_CANNOT_HANDLE:
-            jerr = gCryptoErrorCodes.cryptoErrorUnsupportedOperation;
-            defaultMsg = "Operation not supported in this configuration";
-            break;
-        case ERROR_DRM_FRAME_TOO_LARGE:
-            jerr = gCryptoErrorCodes.cryptoErrorFrameTooLarge;
-            defaultMsg = "Decrytped frame exceeds size of output buffer";
-            break;
-        case ERROR_DRM_SESSION_LOST_STATE:
-            jerr = gCryptoErrorCodes.cryptoErrorLostState;
-            defaultMsg = "Session state was lost, open a new session and retry";
-            break;
-        default:  /* Other negative DRM error codes go out best-effort. */
-            jerr = MediaErrorToJavaError(err);
-            defaultMsg = StrCryptoError(err);
-            break;
-    }
-
-    std::string originalMsg(msg != NULL ? msg : defaultMsg.c_str());
-    DrmStatus dStatus(err, originalMsg.c_str());
-    std::string detailedMsg(DrmUtils::GetExceptionMessage(dStatus, defaultMsg.c_str(), crypto));
-    jstring msgObj = env->NewStringUTF(detailedMsg.c_str());
-
-    jthrowable exception =
-        (jthrowable)env->NewObject(clazz.get(), constructID, msgObj, jerr,
-                                   dStatus.getCdmErr(), dStatus.getOemErr(), dStatus.getContext());
-
+    jthrowable exception = createCryptoException(
+            env, err, msg, crypto, /* cryptoInfo */ NULL);
     env->Throw(exception);
 }
 
@@ -2963,6 +3087,12 @@
     clazz.reset(env->FindClass("android/media/MediaCodec$CryptoInfo"));
     CHECK(clazz.get() != NULL);
 
+    gFields.cryptoInfoSetID = env->GetMethodID(clazz.get(), "set", "(I[I[I[B[BI)V");
+    CHECK(gFields.cryptoInfoSetID != NULL);
+
+    gFields.cryptoInfoSetPatternID = env->GetMethodID(clazz.get(), "setPattern", "(II)V");
+    CHECK(gFields.cryptoInfoSetPatternID != NULL);
+
     gFields.cryptoInfoNumSubSamplesID =
         env->GetFieldID(clazz.get(), "numSubSamples", "I");
     CHECK(gFields.cryptoInfoNumSubSamplesID != NULL);