// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.media;

import android.media.MediaCrypto;
import android.media.MediaDrm;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.util.Log;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.UUID;

/**
 * A wrapper of the android MediaDrm class. Each MediaDrmBridge manages multiple
 * sessions for a single MediaSourcePlayer.
 */
@JNINamespace("media")
class MediaDrmBridge {
    // Implementation Notes:
    // - A media crypto session (mMediaCryptoSession) is opened after MediaDrm
    //   is created. This session will be added to mSessionIds.
    //   a) In multiple session mode, this session will only be used to create
    //      the MediaCrypto object. It's associated mime type is always null and
    //      it's session ID is always INVALID_SESSION_ID.
    //   b) In single session mode, this session will be used to create the
    //      MediaCrypto object and will be used to call getKeyRequest() and
    //      manage all keys.  The session ID will always be the lastest session
    //      ID passed by the caller.
    // - Each createSession() call creates a new session. All sessions are
    //   managed in mSessionIds.
    // - Whenever NotProvisionedException is thrown, we will clean up the
    //   current state and start the provisioning process.
    // - When provisioning is finished, we will try to resume suspended
    //   operations:
    //   a) Create the media crypto session if it's not created.
    //   b) Finish createSession() if previous createSession() was interrupted
    //      by a NotProvisionedException.
    // - Whenever an unexpected error occurred, we'll call release() to release
    //   all resources and clear all states. In that case all calls to this
    //   object will be no-op. All public APIs and callbacks should check
    //   mMediaBridge to make sure release() hasn't been called. Also, we call
    //   release() immediately after the error happens (e.g. after mMediaDrm)
    //   calls. Indirect calls should not call release() again to avoid
    //   duplication (even though it doesn't hurt to call release() twice).

    private static final String TAG = "MediaDrmBridge";
    private static final String SECURITY_LEVEL = "securityLevel";
    private static final String PRIVACY_MODE = "privacyMode";
    private static final String SESSION_SHARING = "sessionSharing";
    private static final String ENABLE = "enable";
    private static final int INVALID_SESSION_ID = 0;

    private MediaDrm mMediaDrm;
    private long mNativeMediaDrmBridge;
    private UUID mSchemeUUID;
    private Handler mHandler;

    // In this mode, we only open one session, i.e. mMediaCryptoSession.
    private boolean mSingleSessionMode;

    // A session only for the purpose of creating a MediaCrypto object.
    // This session is opened when createSession() is called for the first
    // time.
    // - In multiple session mode, all following createSession() calls
    // should create a new session and use it to call getKeyRequest(). No
    // getKeyRequest() should ever be called on this media crypto session.
    // - In single session mode, all createSession() calls use the same
    // media crypto session. When createSession() is called with a new
    // initData, previously added keys may not be available anymore.
    private ByteBuffer mMediaCryptoSession;
    private MediaCrypto mMediaCrypto;

    // The map of all opened sessions to their session reference IDs.
    private HashMap<ByteBuffer, Integer> mSessionIds;
    // The map of all opened sessions to their mime types.
    private HashMap<ByteBuffer, String> mSessionMimeTypes;

    // The queue of all pending createSession() data.
    private ArrayDeque<PendingCreateSessionData> mPendingCreateSessionDataQueue;

    private boolean mResetDeviceCredentialsPending;

    // MediaDrmBridge is waiting for provisioning response from the server.
    //
    // Notes about NotProvisionedException: This exception can be thrown in a
    // lot of cases. To streamline implementation, we do not catch it in private
    // non-native methods and only catch it in public APIs.
    private boolean mProvisioningPending;

    /**
     *  This class contains data needed to call createSession().
     */
    private static class PendingCreateSessionData {
        private final int mSessionId;
        private final byte[] mInitData;
        private final String mMimeType;

        private PendingCreateSessionData(int sessionId, byte[] initData, String mimeType) {
            mSessionId = sessionId;
            mInitData = initData;
            mMimeType = mimeType;
        }

        private int sessionId() { return mSessionId; }
        private byte[] initData() { return mInitData; }
        private String mimeType() { return mMimeType; }
    }

    private static UUID getUUIDFromBytes(byte[] data) {
        if (data.length != 16) {
            return null;
        }
        long mostSigBits = 0;
        long leastSigBits = 0;
        for (int i = 0; i < 8; i++) {
            mostSigBits = (mostSigBits << 8) | (data[i] & 0xff);
        }
        for (int i = 8; i < 16; i++) {
            leastSigBits = (leastSigBits << 8) | (data[i] & 0xff);
        }
        return new UUID(mostSigBits, leastSigBits);
    }

    /**
     *  Gets session associated with the sessionId.
     *
     *  @return session if sessionId maps a valid opened session. Returns null
     *  otherwise.
     */
    private ByteBuffer getSession(int sessionId) {
        for (ByteBuffer session : mSessionIds.keySet()) {
            if (mSessionIds.get(session) == sessionId) {
                return session;
            }
        }
        return null;
    }

    private MediaDrmBridge(UUID schemeUUID, long nativeMediaDrmBridge, boolean singleSessionMode)
            throws android.media.UnsupportedSchemeException {
        mSchemeUUID = schemeUUID;
        mMediaDrm = new MediaDrm(schemeUUID);
        mNativeMediaDrmBridge = nativeMediaDrmBridge;
        mHandler = new Handler();
        mSingleSessionMode = singleSessionMode;
        mSessionIds = new HashMap<ByteBuffer, Integer>();
        mSessionMimeTypes = new HashMap<ByteBuffer, String>();
        mPendingCreateSessionDataQueue = new ArrayDeque<PendingCreateSessionData>();
        mResetDeviceCredentialsPending = false;
        mProvisioningPending = false;

        mMediaDrm.setOnEventListener(new MediaDrmListener());
        mMediaDrm.setPropertyString(PRIVACY_MODE, ENABLE);
        if (!mSingleSessionMode) {
            mMediaDrm.setPropertyString(SESSION_SHARING, ENABLE);
        }

        // We could open a MediaCrypto session here to support faster start of
        // clear lead (no need to wait for createSession()). But on
        // Android, memory and battery resources are precious and we should
        // only create a session when we are sure we'll use it.
        // TODO(xhwang): Investigate other options to support fast start.
    }

    /**
     * Create a MediaCrypto object.
     *
     * @return whether a MediaCrypto object is successfully created.
     */
    private boolean createMediaCrypto() throws android.media.NotProvisionedException {
        if (mMediaDrm == null) {
            return false;
        }
        assert !mProvisioningPending;
        assert mMediaCryptoSession == null;
        assert mMediaCrypto == null;

        // Open media crypto session.
        mMediaCryptoSession = openSession();
        if (mMediaCryptoSession == null) {
            Log.e(TAG, "Cannot create MediaCrypto Session.");
            return false;
        }
        Log.d(TAG, "MediaCrypto Session created: " + mMediaCryptoSession);

        // Create MediaCrypto object.
        try {
            if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) {
                final byte[] mediaCryptoSession = mMediaCryptoSession.array();
                mMediaCrypto = new MediaCrypto(mSchemeUUID, mediaCryptoSession);
                assert mMediaCrypto != null;
                Log.d(TAG, "MediaCrypto successfully created!");
                mSessionIds.put(mMediaCryptoSession, INVALID_SESSION_ID);
                // Notify the native code that MediaCrypto is ready.
                nativeOnMediaCryptoReady(mNativeMediaDrmBridge);
                return true;
            } else {
                Log.e(TAG, "Cannot create MediaCrypto for unsupported scheme.");
            }
        } catch (android.media.MediaCryptoException e) {
            Log.e(TAG, "Cannot create MediaCrypto", e);
        }

        release();
        return false;
    }

    /**
     * Open a new session..
     *
     * @return the session opened. Returns null if unexpected error happened.
     */
    private ByteBuffer openSession() throws android.media.NotProvisionedException {
        assert mMediaDrm != null;
        try {
            byte[] session = mMediaDrm.openSession();
            // ByteBuffer.wrap() is backed by the byte[]. Make a clone here in
            // case the underlying byte[] is modified.
            return ByteBuffer.wrap(session.clone());
        } catch (java.lang.RuntimeException e) {  // TODO(xhwang): Drop this?
            Log.e(TAG, "Cannot open a new session", e);
            release();
            return null;
        }
    }

    /**
     * Close a session.
     *
     * @param session to be closed.
     */
    private void closeSession(ByteBuffer session) {
        assert mMediaDrm != null;
        mMediaDrm.closeSession(session.array());
    }

    /**
     * Check whether the crypto scheme is supported for the given container.
     * If |containerMimeType| is an empty string, we just return whether
     * the crypto scheme is supported.
     * TODO(qinmin): Implement the checking for container.
     *
     * @return true if the container and the crypto scheme is supported, or
     * false otherwise.
     */
    @CalledByNative
    private static boolean isCryptoSchemeSupported(byte[] schemeUUID, String containerMimeType) {
        UUID cryptoScheme = getUUIDFromBytes(schemeUUID);
        return MediaDrm.isCryptoSchemeSupported(cryptoScheme);
    }

    /**
     * Create a new MediaDrmBridge from the crypto scheme UUID.
     *
     * @param schemeUUID Crypto scheme UUID.
     * @param securityLevel Security level to be used.
     * @param nativeMediaDrmBridge Native object of this class.
     */
    @CalledByNative
    private static MediaDrmBridge create(byte[] schemeUUID, int nativeMediaDrmBridge) {
        UUID cryptoScheme = getUUIDFromBytes(schemeUUID);
        if (cryptoScheme == null || !MediaDrm.isCryptoSchemeSupported(cryptoScheme)) {
            return null;
        }

        boolean singleSessionMode = false;
        if (Build.VERSION.RELEASE.equals("4.4")) {
            singleSessionMode = true;
        }
        Log.d(TAG, "MediaDrmBridge uses " +
                (singleSessionMode ? "single" : "multiple") + "-session mode.");

        MediaDrmBridge mediaDrmBridge = null;
        try {
            mediaDrmBridge = new MediaDrmBridge(
                cryptoScheme, nativeMediaDrmBridge, singleSessionMode);
            Log.d(TAG, "MediaDrmBridge successfully created.");
        } catch (android.media.UnsupportedSchemeException e) {
            Log.e(TAG, "Unsupported DRM scheme", e);
        } catch (java.lang.IllegalArgumentException e) {
            Log.e(TAG, "Failed to create MediaDrmBridge", e);
        } catch (java.lang.IllegalStateException e) {
            Log.e(TAG, "Failed to create MediaDrmBridge", e);
        }

        return mediaDrmBridge;
    }

    /**
     * Set the security level that the MediaDrm object uses.
     * This function should be called right after we construct MediaDrmBridge
     * and before we make any other calls.
     */
    @CalledByNative
    private boolean setSecurityLevel(String securityLevel) {
        if (mMediaDrm == null || mMediaCrypto != null) {
            return false;
        }

        String currentSecurityLevel = mMediaDrm.getPropertyString(SECURITY_LEVEL);
        Log.e(TAG, "Security level: current " + currentSecurityLevel + ", new " + securityLevel);
        if (securityLevel.equals(currentSecurityLevel)) {
            // No need to set the same security level again. This is not just
            // a shortcut! Setting the same security level actually causes an
            // exception in MediaDrm!
            return true;
        }

        try {
            mMediaDrm.setPropertyString(SECURITY_LEVEL, securityLevel);
            return true;
        } catch (java.lang.IllegalArgumentException e) {
            Log.e(TAG, "Failed to set security level " + securityLevel, e);
        } catch (java.lang.IllegalStateException e) {
            Log.e(TAG, "Failed to set security level " + securityLevel, e);
        }

        Log.e(TAG, "Security level " + securityLevel + " not supported!");
        return false;
    }

    /**
     * Return the MediaCrypto object if available.
     */
    @CalledByNative
    private MediaCrypto getMediaCrypto() {
        return mMediaCrypto;
    }

    /**
     * Reset the device DRM credentials.
     */
    @CalledByNative
    private void resetDeviceCredentials() {
        mResetDeviceCredentialsPending = true;
        MediaDrm.ProvisionRequest request = mMediaDrm.getProvisionRequest();
        PostRequestTask postTask = new PostRequestTask(request.getData());
        postTask.execute(request.getDefaultUrl());
    }

    /**
     * Release the MediaDrmBridge object.
     */
    @CalledByNative
    private void release() {
        // Do not reset mHandler and mNativeMediaDrmBridge so that we can still
        // post KeyError back to native code.

        mPendingCreateSessionDataQueue.clear();
        mPendingCreateSessionDataQueue = null;

        for (ByteBuffer session : mSessionIds.keySet()) {
            closeSession(session);
        }
        mSessionIds.clear();
        mSessionIds = null;
        mSessionMimeTypes.clear();
        mSessionMimeTypes = null;

        // This session was closed in the "for" loop above.
        mMediaCryptoSession = null;

        if (mMediaCrypto != null) {
            mMediaCrypto.release();
            mMediaCrypto = null;
        }

        if (mMediaDrm != null) {
            mMediaDrm.release();
            mMediaDrm = null;
        }
    }

    /**
     * Get a key request.
     *
     * @param session Session on which we need to get the key request.
     * @param data Data needed to get the key request.
     * @param mime Mime type to get the key request.
     *
     * @return the key request.
     */
    private MediaDrm.KeyRequest getKeyRequest(ByteBuffer session, byte[] data, String mime)
            throws android.media.NotProvisionedException {
        assert mMediaDrm != null;
        assert mMediaCrypto != null;
        assert !mProvisioningPending;

        HashMap<String, String> optionalParameters = new HashMap<String, String>();
        MediaDrm.KeyRequest request = mMediaDrm.getKeyRequest(
                session.array(), data, mime, MediaDrm.KEY_TYPE_STREAMING, optionalParameters);
        String result = (request != null) ? "successed" : "failed";
        Log.d(TAG, "getKeyRequest " + result + "!");
        return request;
    }

    /**
     * Save data to |mPendingCreateSessionDataQueue| so that we can resume the
     * createSession() call later.
     */
    private void savePendingCreateSessionData(int sessionId, byte[] initData, String mime) {
        Log.d(TAG, "savePendingCreateSessionData()");
        mPendingCreateSessionDataQueue.offer(
                new PendingCreateSessionData(sessionId, initData, mime));
    }

    /**
     * Process all pending createSession() calls synchronously.
     */
    private void processPendingCreateSessionData() {
        Log.d(TAG, "processPendingCreateSessionData()");
        assert mMediaDrm != null;

        // Check mMediaDrm != null because error may happen in createSession().
        // Check !mProvisioningPending because NotProvisionedException may be
        // thrown in createSession().
        while (mMediaDrm != null && !mProvisioningPending &&
                !mPendingCreateSessionDataQueue.isEmpty()) {
            PendingCreateSessionData pendingData = mPendingCreateSessionDataQueue.poll();
            int sessionId = pendingData.sessionId();
            byte[] initData = pendingData.initData();
            String mime = pendingData.mimeType();
            createSession(sessionId, initData, mime);
        }
    }

    /**
     * Process pending operations asynchrnously.
     */
    private void resumePendingOperations() {
        mHandler.post(new Runnable(){
            @Override
            public void run() {
                processPendingCreateSessionData();
            }
        });
    }

    /**
     * Create a session with |sessionId|, |initData| and |mime|.
     * In multiple session mode, a new session will be open. In single session
     * mode, the mMediaCryptoSession will be used.
     *
     * @param sessionId ID for the session to be created.
     * @param initData Data needed to generate the key request.
     * @param mime Mime type.
     */
    @CalledByNative
    private void createSession(int sessionId, byte[] initData, String mime) {
        Log.d(TAG, "createSession()");
        if (mMediaDrm == null) {
            Log.e(TAG, "createSession() called when MediaDrm is null.");
            return;
        }

        if (mProvisioningPending) {
            assert mMediaCrypto == null;
            savePendingCreateSessionData(sessionId, initData, mime);
            return;
        }

        boolean newSessionOpened = false;
        ByteBuffer session = null;
        try {
            // Create MediaCrypto if necessary.
            if (mMediaCrypto == null && !createMediaCrypto()) {
              onSessionError(sessionId);
                return;
            }
            assert mMediaCrypto != null;
            assert mSessionIds.containsKey(mMediaCryptoSession);

            if (mSingleSessionMode) {
                session = mMediaCryptoSession;
                if (mSessionMimeTypes.get(session) != null &&
                        !mSessionMimeTypes.get(session).equals(mime)) {
                    Log.e(TAG, "Only one mime type is supported in single session mode.");
                    onSessionError(sessionId);
                    return;
                }
            } else {
                session = openSession();
                if (session == null) {
                    Log.e(TAG, "Cannot open session in createSession().");
                    onSessionError(sessionId);
                    return;
                }
                newSessionOpened = true;
                assert !mSessionIds.containsKey(session);
            }

            MediaDrm.KeyRequest request = null;
            request = getKeyRequest(session, initData, mime);
            if (request == null) {
                if (newSessionOpened) {
                    closeSession(session);
                }
                onSessionError(sessionId);
                return;
            }

            onSessionCreated(sessionId, getWebSessionId(session));
            onSessionMessage(sessionId, request);
            if (newSessionOpened) {
                Log.d(TAG, "createSession(): Session " + getWebSessionId(session) +
                        " (" + sessionId + ") created.");
            }

            mSessionIds.put(session, sessionId);
            mSessionMimeTypes.put(session, mime);
        } catch (android.media.NotProvisionedException e) {
            Log.e(TAG, "Device not provisioned", e);
            if (newSessionOpened) {
                closeSession(session);
            }
            savePendingCreateSessionData(sessionId, initData, mime);
            startProvisioning();
        }
    }

    /**
     * Returns whether |sessionId| is a valid key session, excluding the media
     * crypto session in multi-session mode.
     *
     * @param sessionId Crypto session Id.
     */
    private boolean sessionExists(ByteBuffer session) {
        if (mMediaCryptoSession == null) {
            assert mSessionIds.isEmpty();
            Log.e(TAG, "Session doesn't exist because media crypto session is not created.");
            return false;
        }
        assert mSessionIds.containsKey(mMediaCryptoSession);

        if (mSingleSessionMode) {
            return mMediaCryptoSession.equals(session);
        }

        return !session.equals(mMediaCryptoSession) && mSessionIds.containsKey(session);
    }

    /**
     * Cancel a key request for a session Id.
     *
     * @param sessionId Reference ID of session to be released.
     */
    @CalledByNative
    private void releaseSession(int sessionId) {
        Log.d(TAG, "releaseSession(): " + sessionId);
        if (mMediaDrm == null) {
            Log.e(TAG, "releaseSession() called when MediaDrm is null.");
            return;
        }

        ByteBuffer session = getSession(sessionId);
        if (session == null) {
            Log.e(TAG, "Invalid sessionId in releaseSession.");
            onSessionError(sessionId);
            return;
        }

        mMediaDrm.removeKeys(session.array());

        // We don't close the media crypto session in single session mode.
        if (!mSingleSessionMode) {
            Log.d(TAG, "Session " + sessionId + "closed.");
            closeSession(session);
            mSessionIds.remove(session);
            onSessionClosed(sessionId);
        }
    }

    /**
     * Add a key for a session Id.
     *
     * @param sessionId Reference ID of session to be updated.
     * @param key Response data from the server.
     */
    @CalledByNative
    private void updateSession(int sessionId, byte[] key) {
        Log.d(TAG, "updateSession(): " + sessionId);
        if (mMediaDrm == null) {
            Log.e(TAG, "updateSession() called when MediaDrm is null.");
            return;
        }

        // TODO(xhwang): We should be able to DCHECK this when WD EME is implemented.
        ByteBuffer session = getSession(sessionId);
        if (!sessionExists(session)) {
            Log.e(TAG, "Invalid session in updateSession.");
            onSessionError(sessionId);
            return;
        }

        try {
            try {
                mMediaDrm.provideKeyResponse(session.array(), key);
            } catch (java.lang.IllegalStateException e) {
                // This is not really an exception. Some error code are incorrectly
                // reported as an exception.
                // TODO(qinmin): remove this exception catch when b/10495563 is fixed.
                Log.e(TAG, "Exception intentionally caught when calling provideKeyResponse()", e);
            }
            onSessionReady(sessionId);
            Log.d(TAG, "Key successfully added for session " + sessionId);
            return;
        } catch (android.media.NotProvisionedException e) {
            // TODO(xhwang): Should we handle this?
            Log.e(TAG, "failed to provide key response", e);
        } catch (android.media.DeniedByServerException e) {
            Log.e(TAG, "failed to provide key response", e);
        }
        onSessionError(sessionId);
        release();
    }

    /**
     * Return the security level of this DRM object.
     */
    @CalledByNative
    private String getSecurityLevel() {
        if (mMediaDrm == null) {
            Log.e(TAG, "getSecurityLevel() called when MediaDrm is null.");
            return null;
        }
        return mMediaDrm.getPropertyString("securityLevel");
    }

    private void startProvisioning() {
        Log.d(TAG, "startProvisioning");
        assert mMediaDrm != null;
        assert !mProvisioningPending;
        mProvisioningPending = true;
        MediaDrm.ProvisionRequest request = mMediaDrm.getProvisionRequest();
        PostRequestTask postTask = new PostRequestTask(request.getData());
        postTask.execute(request.getDefaultUrl());
    }

    /**
     * Called when the provision response is received.
     *
     * @param response Response data from the provision server.
     */
    private void onProvisionResponse(byte[] response) {
        Log.d(TAG, "onProvisionResponse()");
        assert mProvisioningPending;
        mProvisioningPending = false;

        // If |mMediaDrm| is released, there is no need to callback native.
        if (mMediaDrm == null) {
            return;
        }

        boolean success = provideProvisionResponse(response);

        if (mResetDeviceCredentialsPending) {
            nativeOnResetDeviceCredentialsCompleted(mNativeMediaDrmBridge, success);
            mResetDeviceCredentialsPending = false;
        }

        if (success) {
            resumePendingOperations();
        }
    }

    /**
     * Provide the provisioning response to MediaDrm.
     * @returns false if the response is invalid or on error, true otherwise.
     */
    boolean provideProvisionResponse(byte[] response) {
        if (response == null || response.length == 0) {
            Log.e(TAG, "Invalid provision response.");
            return false;
        }

        try {
            mMediaDrm.provideProvisionResponse(response);
            return true;
        } catch (android.media.DeniedByServerException e) {
            Log.e(TAG, "failed to provide provision response", e);
        } catch (java.lang.IllegalStateException e) {
            Log.e(TAG, "failed to provide provision response", e);
        }
        return false;
    }

    private void onSessionCreated(final int sessionId, final String webSessionId) {
        mHandler.post(new Runnable(){
            @Override
            public void run() {
                nativeOnSessionCreated(mNativeMediaDrmBridge, sessionId, webSessionId);
            }
        });
    }

    private void onSessionMessage(final int sessionId, final MediaDrm.KeyRequest request) {
        mHandler.post(new Runnable(){
            @Override
            public void run() {
                nativeOnSessionMessage(mNativeMediaDrmBridge, sessionId,
                        request.getData(), request.getDefaultUrl());
            }
        });
    }

    private void onSessionReady(final int sessionId) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                nativeOnSessionReady(mNativeMediaDrmBridge, sessionId);
            }
        });
    }

    private void onSessionClosed(final int sessionId) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                nativeOnSessionClosed(mNativeMediaDrmBridge, sessionId);
            }
        });
    }

    private void onSessionError(final int sessionId) {
        // TODO(qinmin): pass the error code to native.
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                nativeOnSessionError(mNativeMediaDrmBridge, sessionId);
            }
        });
    }

    private String getWebSessionId(ByteBuffer session) {
        String webSessionId = null;
        try {
            webSessionId = new String(session.array(), "UTF-8");
        } catch (java.io.UnsupportedEncodingException e) {
            Log.e(TAG, "getWebSessionId failed", e);
        } catch (java.lang.NullPointerException e) {
            Log.e(TAG, "getWebSessionId failed", e);
        }
        return webSessionId;
    }

    private class MediaDrmListener implements MediaDrm.OnEventListener {
        @Override
        public void onEvent(
                MediaDrm mediaDrm, byte[] session_array, int event, int extra, byte[] data) {
            if (session_array == null) {
                Log.e(TAG, "MediaDrmListener: Null session.");
                return;
            }
            ByteBuffer session = ByteBuffer.wrap(session_array);
            if (!sessionExists(session)) {
                Log.e(TAG, "MediaDrmListener: Invalid session.");
                return;
            }
            Integer sessionId = mSessionIds.get(session);
            if (sessionId == null || sessionId == INVALID_SESSION_ID) {
                Log.e(TAG, "MediaDrmListener: Invalid session ID.");
                return;
            }
            switch(event) {
                case MediaDrm.EVENT_PROVISION_REQUIRED:
                    Log.d(TAG, "MediaDrm.EVENT_PROVISION_REQUIRED");
                    break;
                case MediaDrm.EVENT_KEY_REQUIRED:
                    Log.d(TAG, "MediaDrm.EVENT_KEY_REQUIRED");
                    if (mProvisioningPending) {
                        return;
                    }
                    String mime = mSessionMimeTypes.get(session);
                    MediaDrm.KeyRequest request = null;
                    try {
                        request = getKeyRequest(session, data, mime);
                    } catch (android.media.NotProvisionedException e) {
                        Log.e(TAG, "Device not provisioned", e);
                        startProvisioning();
                        return;
                    }
                    if (request != null) {
                        onSessionMessage(sessionId, request);
                    } else {
                        onSessionError(sessionId);
                    }
                    break;
                case MediaDrm.EVENT_KEY_EXPIRED:
                    Log.d(TAG, "MediaDrm.EVENT_KEY_EXPIRED");
                    onSessionError(sessionId);
                    break;
                case MediaDrm.EVENT_VENDOR_DEFINED:
                    Log.d(TAG, "MediaDrm.EVENT_VENDOR_DEFINED");
                    assert false;  // Should never happen.
                    break;
                default:
                    Log.e(TAG, "Invalid DRM event " + event);
                    return;
            }
        }
    }

    private class PostRequestTask extends AsyncTask<String, Void, Void> {
        private static final String TAG = "PostRequestTask";

        private byte[] mDrmRequest;
        private byte[] mResponseBody;

        public PostRequestTask(byte[] drmRequest) {
            mDrmRequest = drmRequest;
        }

        @Override
        protected Void doInBackground(String... urls) {
            mResponseBody = postRequest(urls[0], mDrmRequest);
            if (mResponseBody != null) {
                Log.d(TAG, "response length=" + mResponseBody.length);
            }
            return null;
        }

        private byte[] postRequest(String url, byte[] drmRequest) {
            HttpClient httpClient = new DefaultHttpClient();
            HttpPost httpPost = new HttpPost(url + "&signedRequest=" + new String(drmRequest));

            Log.d(TAG, "PostRequest:" + httpPost.getRequestLine());
            try {
                // Add data
                httpPost.setHeader("Accept", "*/*");
                httpPost.setHeader("User-Agent", "Widevine CDM v1.0");
                httpPost.setHeader("Content-Type", "application/json");

                // Execute HTTP Post Request
                HttpResponse response = httpClient.execute(httpPost);

                byte[] responseBody;
                int responseCode = response.getStatusLine().getStatusCode();
                if (responseCode == 200) {
                    responseBody = EntityUtils.toByteArray(response.getEntity());
                } else {
                    Log.d(TAG, "Server returned HTTP error code " + responseCode);
                    return null;
                }
                return responseBody;
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void v) {
            onProvisionResponse(mResponseBody);
        }
    }

    private native void nativeOnMediaCryptoReady(long nativeMediaDrmBridge);

    private native void nativeOnSessionCreated(long nativeMediaDrmBridge, int sessionId,
                                               String webSessionId);

    private native void nativeOnSessionMessage(long nativeMediaDrmBridge, int sessionId,
                                               byte[] message, String destinationUrl);

    private native void nativeOnSessionReady(long nativeMediaDrmBridge, int sessionId);

    private native void nativeOnSessionClosed(long nativeMediaDrmBridge, int sessionId);

    private native void nativeOnSessionError(long nativeMediaDrmBridge, int sessionId);

    private native void nativeOnResetDeviceCredentialsCompleted(
            long nativeMediaDrmBridge, boolean success);
}
