root/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. JNINamespace
  2. sessionId
  3. initData
  4. mimeType
  5. getUUIDFromBytes
  6. getSession
  7. createMediaCrypto
  8. openSession
  9. closeSession
  10. isCryptoSchemeSupported
  11. create
  12. setSecurityLevel
  13. getMediaCrypto
  14. resetDeviceCredentials
  15. release
  16. getKeyRequest
  17. savePendingCreateSessionData
  18. processPendingCreateSessionData
  19. resumePendingOperations
  20. createSession
  21. sessionExists
  22. releaseSession
  23. updateSession
  24. getSecurityLevel
  25. startProvisioning
  26. onProvisionResponse
  27. provideProvisionResponse
  28. onSessionCreated
  29. onSessionMessage
  30. onSessionReady
  31. onSessionClosed
  32. onSessionError
  33. getWebSessionId
  34. onEvent
  35. doInBackground
  36. postRequest
  37. onPostExecute
  38. nativeOnMediaCryptoReady
  39. nativeOnSessionCreated
  40. nativeOnSessionMessage
  41. nativeOnSessionReady
  42. nativeOnSessionClosed
  43. nativeOnSessionError
  44. nativeOnResetDeviceCredentialsCompleted

// 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(xhwang): Implement container check. See: http://crbug.com/350481
     *
     * @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);
}

/* [<][>][^][v][top][bottom][index][help] */