/*
 * Copyright 2013, 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.
 */

//#define LOG_NDEBUG 0
#define LOG_TAG "MediaDrm-JNI"
#include <utils/Log.h>

#include "android_media_MediaDrm.h"

#include "android_runtime/AndroidRuntime.h"
#include "android_os_Parcel.h"
#include "jni.h"
#include "JNIHelp.h"

#include <binder/IServiceManager.h>
#include <binder/Parcel.h>
#include <media/IDrm.h>
#include <media/IMediaPlayerService.h>
#include <media/stagefright/foundation/ADebug.h>
#include <media/stagefright/MediaErrors.h>

namespace android {

#define FIND_CLASS(var, className) \
    var = env->FindClass(className); \
    LOG_FATAL_IF(! var, "Unable to find class " className);

#define GET_FIELD_ID(var, clazz, fieldName, fieldDescriptor) \
    var = env->GetFieldID(clazz, fieldName, fieldDescriptor); \
    LOG_FATAL_IF(! var, "Unable to find field " fieldName);

#define GET_METHOD_ID(var, clazz, fieldName, fieldDescriptor) \
    var = env->GetMethodID(clazz, fieldName, fieldDescriptor); \
    LOG_FATAL_IF(! var, "Unable to find method " fieldName);

#define GET_STATIC_FIELD_ID(var, clazz, fieldName, fieldDescriptor) \
    var = env->GetStaticFieldID(clazz, fieldName, fieldDescriptor); \
    LOG_FATAL_IF(! var, "Unable to find field " fieldName);

#define GET_STATIC_METHOD_ID(var, clazz, fieldName, fieldDescriptor) \
    var = env->GetStaticMethodID(clazz, fieldName, fieldDescriptor); \
    LOG_FATAL_IF(! var, "Unable to find static method " fieldName);


struct RequestFields {
    jfieldID data;
    jfieldID defaultUrl;
};

struct ArrayListFields {
    jmethodID init;
    jmethodID add;
};

struct HashmapFields {
    jmethodID init;
    jmethodID get;
    jmethodID put;
    jmethodID entrySet;
};

struct SetFields {
    jmethodID iterator;
};

struct IteratorFields {
    jmethodID next;
    jmethodID hasNext;
};

struct EntryFields {
    jmethodID getKey;
    jmethodID getValue;
};

struct EventTypes {
    jint kEventProvisionRequired;
    jint kEventKeyRequired;
    jint kEventKeyExpired;
    jint kEventVendorDefined;
} gEventTypes;

struct KeyTypes {
    jint kKeyTypeStreaming;
    jint kKeyTypeOffline;
    jint kKeyTypeRelease;
} gKeyTypes;

struct fields_t {
    jfieldID context;
    jmethodID post_event;
    RequestFields keyRequest;
    RequestFields provisionRequest;
    ArrayListFields arraylist;
    HashmapFields hashmap;
    SetFields set;
    IteratorFields iterator;
    EntryFields entry;
};

static fields_t gFields;

// ----------------------------------------------------------------------------
// ref-counted object for callbacks
class JNIDrmListener: public DrmListener
{
public:
    JNIDrmListener(JNIEnv* env, jobject thiz, jobject weak_thiz);
    ~JNIDrmListener();
    virtual void notify(DrmPlugin::EventType eventType, int extra, const Parcel *obj = NULL);
private:
    JNIDrmListener();
    jclass      mClass;     // Reference to MediaDrm class
    jobject     mObject;    // Weak ref to MediaDrm Java object to call on
};

JNIDrmListener::JNIDrmListener(JNIEnv* env, jobject thiz, jobject weak_thiz)
{
    // Hold onto the MediaDrm class for use in calling the static method
    // that posts events to the application thread.
    jclass clazz = env->GetObjectClass(thiz);
    if (clazz == NULL) {
        ALOGE("Can't find android/media/MediaDrm");
        jniThrowException(env, "java/lang/Exception",
                          "Can't find android/media/MediaDrm");
        return;
    }
    mClass = (jclass)env->NewGlobalRef(clazz);

    // We use a weak reference so the MediaDrm object can be garbage collected.
    // The reference is only used as a proxy for callbacks.
    mObject  = env->NewGlobalRef(weak_thiz);
}

JNIDrmListener::~JNIDrmListener()
{
    // remove global references
    JNIEnv *env = AndroidRuntime::getJNIEnv();
    env->DeleteGlobalRef(mObject);
    env->DeleteGlobalRef(mClass);
}

void JNIDrmListener::notify(DrmPlugin::EventType eventType, int extra,
                            const Parcel *obj)
{
    jint jeventType;

    // translate DrmPlugin event types into their java equivalents
    switch(eventType) {
        case DrmPlugin::kDrmPluginEventProvisionRequired:
            jeventType = gEventTypes.kEventProvisionRequired;
            break;
        case DrmPlugin::kDrmPluginEventKeyNeeded:
            jeventType = gEventTypes.kEventKeyRequired;
            break;
        case DrmPlugin::kDrmPluginEventKeyExpired:
            jeventType = gEventTypes.kEventKeyExpired;
            break;
        case DrmPlugin::kDrmPluginEventVendorDefined:
            jeventType = gEventTypes.kEventVendorDefined;
            break;
        default:
            ALOGE("Invalid event DrmPlugin::EventType %d, ignored", (int)eventType);
            return;
    }

    JNIEnv *env = AndroidRuntime::getJNIEnv();
    if (obj && obj->dataSize() > 0) {
        jobject jParcel = createJavaParcelObject(env);
        if (jParcel != NULL) {
            Parcel* nativeParcel = parcelForJavaObject(env, jParcel);
            nativeParcel->setData(obj->data(), obj->dataSize());
            env->CallStaticVoidMethod(mClass, gFields.post_event, mObject,
                    jeventType, extra, jParcel);
        }
    }

    if (env->ExceptionCheck()) {
        ALOGW("An exception occurred while notifying an event.");
        LOGW_EX(env);
        env->ExceptionClear();
    }
}


static bool throwExceptionAsNecessary(
        JNIEnv *env, status_t err, const char *msg = NULL) {

    const char *drmMessage = NULL;

    switch(err) {
    case ERROR_DRM_UNKNOWN:
        drmMessage = "General DRM error";
        break;
    case ERROR_DRM_NO_LICENSE:
        drmMessage = "No license";
        break;
    case ERROR_DRM_LICENSE_EXPIRED:
        drmMessage = "License expired";
        break;
    case ERROR_DRM_SESSION_NOT_OPENED:
        drmMessage = "Session not opened";
        break;
    case ERROR_DRM_DECRYPT_UNIT_NOT_INITIALIZED:
        drmMessage = "Not initialized";
        break;
    case ERROR_DRM_DECRYPT:
        drmMessage = "Decrypt error";
        break;
    case ERROR_DRM_CANNOT_HANDLE:
        drmMessage = "Unsupported scheme or data format";
        break;
    case ERROR_DRM_TAMPER_DETECTED:
        drmMessage = "Invalid state";
        break;
    default:
        break;
    }

    String8 vendorMessage;
    if (err >= ERROR_DRM_VENDOR_MIN && err <= ERROR_DRM_VENDOR_MAX) {
        vendorMessage.format("DRM vendor-defined error: %d", err);
        drmMessage = vendorMessage.string();
    }

    if (err == BAD_VALUE) {
        jniThrowException(env, "java/lang/IllegalArgumentException", msg);
        return true;
    } else if (err == ERROR_DRM_NOT_PROVISIONED) {
        jniThrowException(env, "android/media/NotProvisionedException", msg);
        return true;
    } else if (err == ERROR_DRM_DEVICE_REVOKED) {
        jniThrowException(env, "android/media/DeniedByServerException", msg);
        return true;
    } else if (err != OK) {
        String8 errbuf;
        if (drmMessage != NULL) {
            if (msg == NULL) {
                msg = drmMessage;
            } else {
                errbuf.format("%s: %s", msg, drmMessage);
                msg = errbuf.string();
            }
        }
        ALOGE("Illegal state exception: %s", msg);
        jniThrowException(env, "java/lang/IllegalStateException", msg);
        return true;
    }
    return false;
}

static sp<IDrm> GetDrm(JNIEnv *env, jobject thiz) {
    JDrm *jdrm = (JDrm *)env->GetIntField(thiz, gFields.context);
    return jdrm ? jdrm->getDrm() : NULL;
}

JDrm::JDrm(
        JNIEnv *env, jobject thiz, const uint8_t uuid[16]) {
    mObject = env->NewWeakGlobalRef(thiz);
    mDrm = MakeDrm(uuid);
    if (mDrm != NULL) {
        mDrm->setListener(this);
    }
}

JDrm::~JDrm() {
    mDrm.clear();

    JNIEnv *env = AndroidRuntime::getJNIEnv();

    env->DeleteWeakGlobalRef(mObject);
    mObject = NULL;
}

// static
sp<IDrm> JDrm::MakeDrm() {
    sp<IServiceManager> sm = defaultServiceManager();

    sp<IBinder> binder =
        sm->getService(String16("media.player"));

    sp<IMediaPlayerService> service =
        interface_cast<IMediaPlayerService>(binder);

    if (service == NULL) {
        return NULL;
    }

    sp<IDrm> drm = service->makeDrm();

    if (drm == NULL || (drm->initCheck() != OK && drm->initCheck() != NO_INIT)) {
        return NULL;
    }

    return drm;
}

// static
sp<IDrm> JDrm::MakeDrm(const uint8_t uuid[16]) {
    sp<IDrm> drm = MakeDrm();

    if (drm == NULL) {
        return NULL;
    }

    status_t err = drm->createPlugin(uuid);

    if (err != OK) {
        return NULL;
    }

    return drm;
}

status_t JDrm::setListener(const sp<DrmListener>& listener) {
    Mutex::Autolock lock(mLock);
    mListener = listener;
    return OK;
}

void JDrm::notify(DrmPlugin::EventType eventType, int extra, const Parcel *obj) {
    sp<DrmListener> listener;
    mLock.lock();
    listener = mListener;
    mLock.unlock();

    if (listener != NULL) {
        Mutex::Autolock lock(mNotifyLock);
        listener->notify(eventType, extra, obj);
    }
}


// static
bool JDrm::IsCryptoSchemeSupported(const uint8_t uuid[16]) {
    sp<IDrm> drm = MakeDrm();

    if (drm == NULL) {
        return false;
    }

    return drm->isCryptoSchemeSupported(uuid);
}

status_t JDrm::initCheck() const {
    return mDrm == NULL ? NO_INIT : OK;
}

// JNI conversion utilities
static Vector<uint8_t> JByteArrayToVector(JNIEnv *env, jbyteArray const &byteArray) {
    Vector<uint8_t> vector;
    size_t length = env->GetArrayLength(byteArray);
    vector.insertAt((size_t)0, length);
    env->GetByteArrayRegion(byteArray, 0, length, (jbyte *)vector.editArray());
    return vector;
}

static jbyteArray VectorToJByteArray(JNIEnv *env, Vector<uint8_t> const &vector) {
    size_t length = vector.size();
    jbyteArray result = env->NewByteArray(length);
    if (result != NULL) {
        env->SetByteArrayRegion(result, 0, length, (jbyte *)vector.array());
    }
    return result;
}

static String8 JStringToString8(JNIEnv *env, jstring const &jstr) {
    String8 result;

    const char *s = env->GetStringUTFChars(jstr, NULL);
    if (s) {
        result = s;
        env->ReleaseStringUTFChars(jstr, s);
    }
    return result;
}

/*
    import java.util.HashMap;
    import java.util.Set;
    import java.Map.Entry;
    import jav.util.Iterator;

    HashMap<k, v> hm;
    Set<Entry<k, v> > s = hm.entrySet();
    Iterator i = s.iterator();
    Entry e = s.next();
*/

static KeyedVector<String8, String8> HashMapToKeyedVector(JNIEnv *env, jobject &hashMap) {
    jclass clazz;
    FIND_CLASS(clazz, "java/lang/String");
    KeyedVector<String8, String8> keyedVector;

    jobject entrySet = env->CallObjectMethod(hashMap, gFields.hashmap.entrySet);
    if (entrySet) {
        jobject iterator = env->CallObjectMethod(entrySet, gFields.set.iterator);
        if (iterator) {
            jboolean hasNext = env->CallBooleanMethod(iterator, gFields.iterator.hasNext);
            while (hasNext) {
                jobject entry = env->CallObjectMethod(iterator, gFields.iterator.next);
                if (entry) {
                    jobject obj = env->CallObjectMethod(entry, gFields.entry.getKey);
                    if (!env->IsInstanceOf(obj, clazz)) {
                        jniThrowException(env, "java/lang/IllegalArgumentException",
                                          "HashMap key is not a String");
                    }
                    jstring jkey = static_cast<jstring>(obj);

                    obj = env->CallObjectMethod(entry, gFields.entry.getValue);
                    if (!env->IsInstanceOf(obj, clazz)) {
                        jniThrowException(env, "java/lang/IllegalArgumentException",
                                          "HashMap value is not a String");
                    }
                    jstring jvalue = static_cast<jstring>(obj);

                    String8 key = JStringToString8(env, jkey);
                    String8 value = JStringToString8(env, jvalue);
                    keyedVector.add(key, value);

                    env->DeleteLocalRef(jkey);
                    env->DeleteLocalRef(jvalue);
                    hasNext = env->CallBooleanMethod(iterator, gFields.iterator.hasNext);
                }
                env->DeleteLocalRef(entry);
            }
            env->DeleteLocalRef(iterator);
        }
        env->DeleteLocalRef(entrySet);
    }
    return keyedVector;
}

static jobject KeyedVectorToHashMap (JNIEnv *env, KeyedVector<String8, String8> const &map) {
    jclass clazz;
    FIND_CLASS(clazz, "java/util/HashMap");
    jobject hashMap = env->NewObject(clazz, gFields.hashmap.init);
    for (size_t i = 0; i < map.size(); ++i) {
        jstring jkey = env->NewStringUTF(map.keyAt(i).string());
        jstring jvalue = env->NewStringUTF(map.valueAt(i).string());
        env->CallObjectMethod(hashMap, gFields.hashmap.put, jkey, jvalue);
        env->DeleteLocalRef(jkey);
        env->DeleteLocalRef(jvalue);
    }
    return hashMap;
}

static jobject ListOfVectorsToArrayListOfByteArray(JNIEnv *env,
                                                   List<Vector<uint8_t> > list) {
    jclass clazz;
    FIND_CLASS(clazz, "java/util/ArrayList");
    jobject arrayList = env->NewObject(clazz, gFields.arraylist.init);
    List<Vector<uint8_t> >::iterator iter = list.begin();
    while (iter != list.end()) {
        jbyteArray byteArray = VectorToJByteArray(env, *iter);
        env->CallBooleanMethod(arrayList, gFields.arraylist.add, byteArray);
        env->DeleteLocalRef(byteArray);
        iter++;
    }

    return arrayList;
}

}  // namespace android

using namespace android;

static sp<JDrm> setDrm(
        JNIEnv *env, jobject thiz, const sp<JDrm> &drm) {
    sp<JDrm> old = (JDrm *)env->GetIntField(thiz, gFields.context);
    if (drm != NULL) {
        drm->incStrong(thiz);
    }
    if (old != NULL) {
        old->decStrong(thiz);
    }
    env->SetIntField(thiz, gFields.context, (int)drm.get());

    return old;
}

static bool CheckSession(JNIEnv *env, const sp<IDrm> &drm, jbyteArray const &jsessionId)
{
    if (drm == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException", "MediaDrm obj is null");
        return false;
    }

    if (jsessionId == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException", "sessionId is null");
        return false;
    }
    return true;
}

static void android_media_MediaDrm_release(JNIEnv *env, jobject thiz) {
    sp<JDrm> drm = setDrm(env, thiz, NULL);
    if (drm != NULL) {
        drm->setListener(NULL);
    }
}

static void android_media_MediaDrm_native_init(JNIEnv *env) {
    jclass clazz;
    FIND_CLASS(clazz, "android/media/MediaDrm");
    GET_FIELD_ID(gFields.context, clazz, "mNativeContext", "I");
    GET_STATIC_METHOD_ID(gFields.post_event, clazz, "postEventFromNative",
                         "(Ljava/lang/Object;IILjava/lang/Object;)V");

    jfieldID field;
    GET_STATIC_FIELD_ID(field, clazz, "EVENT_PROVISION_REQUIRED", "I");
    gEventTypes.kEventProvisionRequired = env->GetStaticIntField(clazz, field);
    GET_STATIC_FIELD_ID(field, clazz, "EVENT_KEY_REQUIRED", "I");
    gEventTypes.kEventKeyRequired = env->GetStaticIntField(clazz, field);
    GET_STATIC_FIELD_ID(field, clazz, "EVENT_KEY_EXPIRED", "I");
    gEventTypes.kEventKeyExpired = env->GetStaticIntField(clazz, field);
    GET_STATIC_FIELD_ID(field, clazz, "EVENT_VENDOR_DEFINED", "I");
    gEventTypes.kEventVendorDefined = env->GetStaticIntField(clazz, field);

    GET_STATIC_FIELD_ID(field, clazz, "KEY_TYPE_STREAMING", "I");
    gKeyTypes.kKeyTypeStreaming = env->GetStaticIntField(clazz, field);
    GET_STATIC_FIELD_ID(field, clazz, "KEY_TYPE_OFFLINE", "I");
    gKeyTypes.kKeyTypeOffline = env->GetStaticIntField(clazz, field);
    GET_STATIC_FIELD_ID(field, clazz, "KEY_TYPE_RELEASE", "I");
    gKeyTypes.kKeyTypeRelease = env->GetStaticIntField(clazz, field);

    FIND_CLASS(clazz, "android/media/MediaDrm$KeyRequest");
    GET_FIELD_ID(gFields.keyRequest.data, clazz, "mData", "[B");
    GET_FIELD_ID(gFields.keyRequest.defaultUrl, clazz, "mDefaultUrl", "Ljava/lang/String;");

    FIND_CLASS(clazz, "android/media/MediaDrm$ProvisionRequest");
    GET_FIELD_ID(gFields.provisionRequest.data, clazz, "mData", "[B");
    GET_FIELD_ID(gFields.provisionRequest.defaultUrl, clazz, "mDefaultUrl", "Ljava/lang/String;");

    FIND_CLASS(clazz, "java/util/ArrayList");
    GET_METHOD_ID(gFields.arraylist.init, clazz, "<init>", "()V");
    GET_METHOD_ID(gFields.arraylist.add, clazz, "add", "(Ljava/lang/Object;)Z");

    FIND_CLASS(clazz, "java/util/HashMap");
    GET_METHOD_ID(gFields.hashmap.init, clazz, "<init>", "()V");
    GET_METHOD_ID(gFields.hashmap.get, clazz, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
    GET_METHOD_ID(gFields.hashmap.put, clazz, "put",
                  "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
    GET_METHOD_ID(gFields.hashmap.entrySet, clazz, "entrySet", "()Ljava/util/Set;");

    FIND_CLASS(clazz, "java/util/Set");
    GET_METHOD_ID(gFields.set.iterator, clazz, "iterator", "()Ljava/util/Iterator;");

    FIND_CLASS(clazz, "java/util/Iterator");
    GET_METHOD_ID(gFields.iterator.next, clazz, "next", "()Ljava/lang/Object;");
    GET_METHOD_ID(gFields.iterator.hasNext, clazz, "hasNext", "()Z");

    FIND_CLASS(clazz, "java/util/Map$Entry");
    GET_METHOD_ID(gFields.entry.getKey, clazz, "getKey", "()Ljava/lang/Object;");
    GET_METHOD_ID(gFields.entry.getValue, clazz, "getValue", "()Ljava/lang/Object;");
}

static void android_media_MediaDrm_native_setup(
        JNIEnv *env, jobject thiz,
        jobject weak_this, jbyteArray uuidObj) {

    if (uuidObj == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException", "uuid is null");
        return;
    }

    Vector<uint8_t> uuid = JByteArrayToVector(env, uuidObj);

    if (uuid.size() != 16) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "invalid UUID size, expected 16 bytes");
        return;
    }

    sp<JDrm> drm = new JDrm(env, thiz, uuid.array());

    status_t err = drm->initCheck();

    if (err != OK) {
        jniThrowException(
                env,
                "android/media/UnsupportedSchemeException",
                "Failed to instantiate drm object.");
        return;
    }

    sp<JNIDrmListener> listener = new JNIDrmListener(env, thiz, weak_this);
    drm->setListener(listener);
    setDrm(env, thiz, drm);
}

static void android_media_MediaDrm_native_finalize(
        JNIEnv *env, jobject thiz) {
    android_media_MediaDrm_release(env, thiz);
}

static jboolean android_media_MediaDrm_isCryptoSchemeSupportedNative(
        JNIEnv *env, jobject thiz, jbyteArray uuidObj) {

    if (uuidObj == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
        return false;
    }

    Vector<uint8_t> uuid = JByteArrayToVector(env, uuidObj);

    if (uuid.size() != 16) {
        jniThrowException(
                env,
                "java/lang/IllegalArgumentException",
                "invalid UUID size, expected 16 bytes");
        return false;
    }

    return JDrm::IsCryptoSchemeSupported(uuid.array());
}

static jbyteArray android_media_MediaDrm_openSession(
    JNIEnv *env, jobject thiz) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (drm == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException",
                          "MediaDrm obj is null");
        return NULL;
    }

    Vector<uint8_t> sessionId;
    status_t err = drm->openSession(sessionId);

    if (throwExceptionAsNecessary(env, err, "Failed to open session")) {
        return NULL;
    }

    return VectorToJByteArray(env, sessionId);
}

static void android_media_MediaDrm_closeSession(
    JNIEnv *env, jobject thiz, jbyteArray jsessionId) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (!CheckSession(env, drm, jsessionId)) {
        return;
    }

    Vector<uint8_t> sessionId(JByteArrayToVector(env, jsessionId));

    status_t err = drm->closeSession(sessionId);

    throwExceptionAsNecessary(env, err, "Failed to close session");
}

static jobject android_media_MediaDrm_getKeyRequest(
    JNIEnv *env, jobject thiz, jbyteArray jsessionId, jbyteArray jinitData,
    jstring jmimeType, jint jkeyType, jobject joptParams) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (!CheckSession(env, drm, jsessionId)) {
        return NULL;
    }

    Vector<uint8_t> sessionId(JByteArrayToVector(env, jsessionId));

    Vector<uint8_t> initData;
    if (jinitData != NULL) {
        initData = JByteArrayToVector(env, jinitData);
    }

    String8 mimeType;
    if (jmimeType != NULL) {
        mimeType = JStringToString8(env, jmimeType);
    }

    DrmPlugin::KeyType keyType;
    if (jkeyType == gKeyTypes.kKeyTypeStreaming) {
        keyType = DrmPlugin::kKeyType_Streaming;
    } else if (jkeyType == gKeyTypes.kKeyTypeOffline) {
        keyType = DrmPlugin::kKeyType_Offline;
    } else if (jkeyType == gKeyTypes.kKeyTypeRelease) {
        keyType = DrmPlugin::kKeyType_Release;
    } else {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "invalid keyType");
        return NULL;
    }

    KeyedVector<String8, String8> optParams;
    if (joptParams != NULL) {
        optParams = HashMapToKeyedVector(env, joptParams);
    }

    Vector<uint8_t> request;
    String8 defaultUrl;

    status_t err = drm->getKeyRequest(sessionId, initData, mimeType,
                                          keyType, optParams, request, defaultUrl);

    if (throwExceptionAsNecessary(env, err, "Failed to get key request")) {
        return NULL;
    }

    // Fill out return obj
    jclass clazz;
    FIND_CLASS(clazz, "android/media/MediaDrm$KeyRequest");

    jobject keyObj = NULL;

    if (clazz) {
        keyObj = env->AllocObject(clazz);
        jbyteArray jrequest = VectorToJByteArray(env, request);
        env->SetObjectField(keyObj, gFields.keyRequest.data, jrequest);

        jstring jdefaultUrl = env->NewStringUTF(defaultUrl.string());
        env->SetObjectField(keyObj, gFields.keyRequest.defaultUrl, jdefaultUrl);
    }

    return keyObj;
}

static jbyteArray android_media_MediaDrm_provideKeyResponse(
    JNIEnv *env, jobject thiz, jbyteArray jsessionId, jbyteArray jresponse) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (!CheckSession(env, drm, jsessionId)) {
        return NULL;
    }

    Vector<uint8_t> sessionId(JByteArrayToVector(env, jsessionId));

    if (jresponse == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "key response is null");
        return NULL;
    }
    Vector<uint8_t> response(JByteArrayToVector(env, jresponse));
    Vector<uint8_t> keySetId;

    status_t err = drm->provideKeyResponse(sessionId, response, keySetId);

    throwExceptionAsNecessary(env, err, "Failed to handle key response");
    return VectorToJByteArray(env, keySetId);
}

static void android_media_MediaDrm_removeKeys(
    JNIEnv *env, jobject thiz, jbyteArray jkeysetId) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (jkeysetId == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "keySetId is null");
        return;
    }

    Vector<uint8_t> keySetId(JByteArrayToVector(env, jkeysetId));

    status_t err = drm->removeKeys(keySetId);

    throwExceptionAsNecessary(env, err, "Failed to remove keys");
}

static void android_media_MediaDrm_restoreKeys(
    JNIEnv *env, jobject thiz, jbyteArray jsessionId,
    jbyteArray jkeysetId) {

    sp<IDrm> drm = GetDrm(env, thiz);

    if (!CheckSession(env, drm, jsessionId)) {
        return;
    }

    if (jkeysetId == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
        return;
    }

    Vector<uint8_t> sessionId(JByteArrayToVector(env, jsessionId));
    Vector<uint8_t> keySetId(JByteArrayToVector(env, jkeysetId));

    status_t err = drm->restoreKeys(sessionId, keySetId);

    throwExceptionAsNecessary(env, err, "Failed to restore keys");
}

static jobject android_media_MediaDrm_queryKeyStatus(
    JNIEnv *env, jobject thiz, jbyteArray jsessionId) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (!CheckSession(env, drm, jsessionId)) {
        return NULL;
    }
    Vector<uint8_t> sessionId(JByteArrayToVector(env, jsessionId));

    KeyedVector<String8, String8> infoMap;

    status_t err = drm->queryKeyStatus(sessionId, infoMap);

    if (throwExceptionAsNecessary(env, err, "Failed to query key status")) {
        return NULL;
    }

    return KeyedVectorToHashMap(env, infoMap);
}

static jobject android_media_MediaDrm_getProvisionRequest(
    JNIEnv *env, jobject thiz) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (drm == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException",
                          "MediaDrm obj is null");
        return NULL;
    }

    Vector<uint8_t> request;
    String8 defaultUrl;

    status_t err = drm->getProvisionRequest(request, defaultUrl);

    if (throwExceptionAsNecessary(env, err, "Failed to get provision request")) {
        return NULL;
    }

    // Fill out return obj
    jclass clazz;
    FIND_CLASS(clazz, "android/media/MediaDrm$ProvisionRequest");

    jobject provisionObj = NULL;

    if (clazz) {
        provisionObj = env->AllocObject(clazz);
        jbyteArray jrequest = VectorToJByteArray(env, request);
        env->SetObjectField(provisionObj, gFields.provisionRequest.data, jrequest);

        jstring jdefaultUrl = env->NewStringUTF(defaultUrl.string());
        env->SetObjectField(provisionObj, gFields.provisionRequest.defaultUrl, jdefaultUrl);
    }

    return provisionObj;
}

static void android_media_MediaDrm_provideProvisionResponse(
    JNIEnv *env, jobject thiz, jbyteArray jresponse) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (drm == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException",
                          "MediaDrm obj is null");
        return;
    }

    if (jresponse == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "provision response is null");
        return;
    }

    Vector<uint8_t> response(JByteArrayToVector(env, jresponse));

    status_t err = drm->provideProvisionResponse(response);

    throwExceptionAsNecessary(env, err, "Failed to handle provision response");
}

static jobject android_media_MediaDrm_getSecureStops(
    JNIEnv *env, jobject thiz) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (drm == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException",
                          "MediaDrm obj is null");
        return NULL;
    }

    List<Vector<uint8_t> > secureStops;

    status_t err = drm->getSecureStops(secureStops);

    if (throwExceptionAsNecessary(env, err, "Failed to get secure stops")) {
        return NULL;
    }

    return ListOfVectorsToArrayListOfByteArray(env, secureStops);
}

static void android_media_MediaDrm_releaseSecureStops(
    JNIEnv *env, jobject thiz, jbyteArray jssRelease) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (drm == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException",
                          "MediaDrm obj is null");
        return;
    }

    Vector<uint8_t> ssRelease(JByteArrayToVector(env, jssRelease));

    status_t err = drm->releaseSecureStops(ssRelease);

    throwExceptionAsNecessary(env, err, "Failed to release secure stops");
}

static jstring android_media_MediaDrm_getPropertyString(
    JNIEnv *env, jobject thiz, jstring jname) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (drm == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException",
                          "MediaDrm obj is null");
        return NULL;
    }

    if (jname == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "property name String is null");
        return NULL;
    }

    String8 name = JStringToString8(env, jname);
    String8 value;

    status_t err = drm->getPropertyString(name, value);

    if (throwExceptionAsNecessary(env, err, "Failed to get property")) {
        return NULL;
    }

    return env->NewStringUTF(value.string());
}

static jbyteArray android_media_MediaDrm_getPropertyByteArray(
    JNIEnv *env, jobject thiz, jstring jname) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (drm == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException",
                          "MediaDrm obj is null");
        return NULL;
    }

    if (jname == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "property name String is null");
        return NULL;
    }

    String8 name = JStringToString8(env, jname);
    Vector<uint8_t> value;

    status_t err = drm->getPropertyByteArray(name, value);

    if (throwExceptionAsNecessary(env, err, "Failed to get property")) {
        return NULL;
    }

    return VectorToJByteArray(env, value);
}

static void android_media_MediaDrm_setPropertyString(
    JNIEnv *env, jobject thiz, jstring jname, jstring jvalue) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (drm == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException",
                          "MediaDrm obj is null");
        return;
    }

    if (jname == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "property name String is null");
        return;
    }

    if (jvalue == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "property value String is null");
        return;
    }

    String8 name = JStringToString8(env, jname);
    String8 value = JStringToString8(env, jvalue);

    status_t err = drm->setPropertyString(name, value);

    throwExceptionAsNecessary(env, err, "Failed to set property");
}

static void android_media_MediaDrm_setPropertyByteArray(
    JNIEnv *env, jobject thiz, jstring jname, jbyteArray jvalue) {
    sp<IDrm> drm = GetDrm(env, thiz);

    if (drm == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException",
                          "MediaDrm obj is null");
        return;
    }

    if (jname == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "property name String is null");
        return;
    }

    if (jvalue == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "property value byte array is null");
        return;
    }

    String8 name = JStringToString8(env, jname);
    Vector<uint8_t> value = JByteArrayToVector(env, jvalue);

    status_t err = drm->setPropertyByteArray(name, value);

    throwExceptionAsNecessary(env, err, "Failed to set property");
}

static void android_media_MediaDrm_setCipherAlgorithmNative(
    JNIEnv *env, jobject thiz, jobject jdrm, jbyteArray jsessionId,
    jstring jalgorithm) {

    sp<IDrm> drm = GetDrm(env, jdrm);

    if (!CheckSession(env, drm, jsessionId)) {
        return;
    }

    if (jalgorithm == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "algorithm String is null");
        return;
    }

    Vector<uint8_t> sessionId(JByteArrayToVector(env, jsessionId));
    String8 algorithm = JStringToString8(env, jalgorithm);

    status_t err = drm->setCipherAlgorithm(sessionId, algorithm);

    throwExceptionAsNecessary(env, err, "Failed to set cipher algorithm");
}

static void android_media_MediaDrm_setMacAlgorithmNative(
    JNIEnv *env, jobject thiz, jobject jdrm, jbyteArray jsessionId,
    jstring jalgorithm) {

    sp<IDrm> drm = GetDrm(env, jdrm);

    if (!CheckSession(env, drm, jsessionId)) {
        return;
    }

    if (jalgorithm == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "algorithm String is null");
        return;
    }

    Vector<uint8_t> sessionId(JByteArrayToVector(env, jsessionId));
    String8 algorithm = JStringToString8(env, jalgorithm);

    status_t err = drm->setMacAlgorithm(sessionId, algorithm);

    throwExceptionAsNecessary(env, err, "Failed to set mac algorithm");
}


static jbyteArray android_media_MediaDrm_encryptNative(
    JNIEnv *env, jobject thiz, jobject jdrm, jbyteArray jsessionId,
    jbyteArray jkeyId, jbyteArray jinput, jbyteArray jiv) {

    sp<IDrm> drm = GetDrm(env, jdrm);

    if (!CheckSession(env, drm, jsessionId)) {
        return NULL;
    }

    if (jkeyId == NULL || jinput == NULL || jiv == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "required argument is null");
        return NULL;
    }

    Vector<uint8_t> sessionId(JByteArrayToVector(env, jsessionId));
    Vector<uint8_t> keyId(JByteArrayToVector(env, jkeyId));
    Vector<uint8_t> input(JByteArrayToVector(env, jinput));
    Vector<uint8_t> iv(JByteArrayToVector(env, jiv));
    Vector<uint8_t> output;

    status_t err = drm->encrypt(sessionId, keyId, input, iv, output);

    throwExceptionAsNecessary(env, err, "Failed to encrypt");

    return VectorToJByteArray(env, output);
}

static jbyteArray android_media_MediaDrm_decryptNative(
    JNIEnv *env, jobject thiz, jobject jdrm, jbyteArray jsessionId,
    jbyteArray jkeyId, jbyteArray jinput, jbyteArray jiv) {

    sp<IDrm> drm = GetDrm(env, jdrm);

    if (!CheckSession(env, drm, jsessionId)) {
        return NULL;
    }

    if (jkeyId == NULL || jinput == NULL || jiv == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "required argument is null");
        return NULL;
    }

    Vector<uint8_t> sessionId(JByteArrayToVector(env, jsessionId));
    Vector<uint8_t> keyId(JByteArrayToVector(env, jkeyId));
    Vector<uint8_t> input(JByteArrayToVector(env, jinput));
    Vector<uint8_t> iv(JByteArrayToVector(env, jiv));
    Vector<uint8_t> output;

    status_t err = drm->decrypt(sessionId, keyId, input, iv, output);
    throwExceptionAsNecessary(env, err, "Failed to decrypt");

    return VectorToJByteArray(env, output);
}

static jbyteArray android_media_MediaDrm_signNative(
    JNIEnv *env, jobject thiz, jobject jdrm, jbyteArray jsessionId,
    jbyteArray jkeyId, jbyteArray jmessage) {

    sp<IDrm> drm = GetDrm(env, jdrm);

    if (!CheckSession(env, drm, jsessionId)) {
        return NULL;
    }

    if (jkeyId == NULL || jmessage == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "required argument is null");
        return NULL;
    }

    Vector<uint8_t> sessionId(JByteArrayToVector(env, jsessionId));
    Vector<uint8_t> keyId(JByteArrayToVector(env, jkeyId));
    Vector<uint8_t> message(JByteArrayToVector(env, jmessage));
    Vector<uint8_t> signature;

    status_t err = drm->sign(sessionId, keyId, message, signature);

    throwExceptionAsNecessary(env, err, "Failed to sign");

    return VectorToJByteArray(env, signature);
}

static jboolean android_media_MediaDrm_verifyNative(
    JNIEnv *env, jobject thiz, jobject jdrm, jbyteArray jsessionId,
    jbyteArray jkeyId, jbyteArray jmessage, jbyteArray jsignature) {

    sp<IDrm> drm = GetDrm(env, jdrm);

    if (!CheckSession(env, drm, jsessionId)) {
        return false;
    }

    if (jkeyId == NULL || jmessage == NULL || jsignature == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "required argument is null");
        return false;
    }

    Vector<uint8_t> sessionId(JByteArrayToVector(env, jsessionId));
    Vector<uint8_t> keyId(JByteArrayToVector(env, jkeyId));
    Vector<uint8_t> message(JByteArrayToVector(env, jmessage));
    Vector<uint8_t> signature(JByteArrayToVector(env, jsignature));
    bool match;

    status_t err = drm->verify(sessionId, keyId, message, signature, match);

    throwExceptionAsNecessary(env, err, "Failed to verify");
    return match;
}


static JNINativeMethod gMethods[] = {
    { "release", "()V", (void *)android_media_MediaDrm_release },
    { "native_init", "()V", (void *)android_media_MediaDrm_native_init },

    { "native_setup", "(Ljava/lang/Object;[B)V",
      (void *)android_media_MediaDrm_native_setup },

    { "native_finalize", "()V",
      (void *)android_media_MediaDrm_native_finalize },

    { "isCryptoSchemeSupportedNative", "([B)Z",
      (void *)android_media_MediaDrm_isCryptoSchemeSupportedNative },

    { "openSession", "()[B",
      (void *)android_media_MediaDrm_openSession },

    { "closeSession", "([B)V",
      (void *)android_media_MediaDrm_closeSession },

    { "getKeyRequest", "([B[BLjava/lang/String;ILjava/util/HashMap;)"
      "Landroid/media/MediaDrm$KeyRequest;",
      (void *)android_media_MediaDrm_getKeyRequest },

    { "provideKeyResponse", "([B[B)[B",
      (void *)android_media_MediaDrm_provideKeyResponse },

    { "removeKeys", "([B)V",
      (void *)android_media_MediaDrm_removeKeys },

    { "restoreKeys", "([B[B)V",
      (void *)android_media_MediaDrm_restoreKeys },

    { "queryKeyStatus", "([B)Ljava/util/HashMap;",
      (void *)android_media_MediaDrm_queryKeyStatus },

    { "getProvisionRequest", "()Landroid/media/MediaDrm$ProvisionRequest;",
      (void *)android_media_MediaDrm_getProvisionRequest },

    { "provideProvisionResponse", "([B)V",
      (void *)android_media_MediaDrm_provideProvisionResponse },

    { "getSecureStops", "()Ljava/util/List;",
      (void *)android_media_MediaDrm_getSecureStops },

    { "releaseSecureStops", "([B)V",
      (void *)android_media_MediaDrm_releaseSecureStops },

    { "getPropertyString", "(Ljava/lang/String;)Ljava/lang/String;",
      (void *)android_media_MediaDrm_getPropertyString },

    { "getPropertyByteArray", "(Ljava/lang/String;)[B",
      (void *)android_media_MediaDrm_getPropertyByteArray },

    { "setPropertyString", "(Ljava/lang/String;Ljava/lang/String;)V",
      (void *)android_media_MediaDrm_setPropertyString },

    { "setPropertyByteArray", "(Ljava/lang/String;[B)V",
      (void *)android_media_MediaDrm_setPropertyByteArray },

    { "setCipherAlgorithmNative",
      "(Landroid/media/MediaDrm;[BLjava/lang/String;)V",
      (void *)android_media_MediaDrm_setCipherAlgorithmNative },

    { "setMacAlgorithmNative",
      "(Landroid/media/MediaDrm;[BLjava/lang/String;)V",
      (void *)android_media_MediaDrm_setMacAlgorithmNative },

    { "encryptNative", "(Landroid/media/MediaDrm;[B[B[B[B)[B",
      (void *)android_media_MediaDrm_encryptNative },

    { "decryptNative", "(Landroid/media/MediaDrm;[B[B[B[B)[B",
      (void *)android_media_MediaDrm_decryptNative },

    { "signNative", "(Landroid/media/MediaDrm;[B[B[B)[B",
      (void *)android_media_MediaDrm_signNative },

    { "verifyNative", "(Landroid/media/MediaDrm;[B[B[B[B)Z",
      (void *)android_media_MediaDrm_verifyNative },
};

int register_android_media_Drm(JNIEnv *env) {
    return AndroidRuntime::registerNativeMethods(env,
                "android/media/MediaDrm", gMethods, NELEM(gMethods));
}