// 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); }