// 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.Handler; import android.util.Log; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.HttpClient; import org.apache.http.client.ClientProtocolException; 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.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 { private static final String TAG = "MediaDrmBridge"; private static final String SECURITY_LEVEL = "securityLevel"; private static final String PRIVACY_MODE = "privacyMode"; private MediaDrm mMediaDrm; private UUID mSchemeUUID; private int mNativeMediaDrmBridge; // TODO(qinmin): we currently only support one session per DRM bridge. // Change this to a HashMap if we start to support multiple sessions. private String mSessionId; private MediaCrypto mMediaCrypto; private String mMimeType; private Handler mHandler; private byte[] mPendingInitData; private boolean mResetDeviceCredentialsPending; 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); } private MediaDrmBridge(UUID schemeUUID, String securityLevel, int nativeMediaDrmBridge) throws android.media.UnsupportedSchemeException { mSchemeUUID = schemeUUID; mMediaDrm = new MediaDrm(schemeUUID); mHandler = new Handler(); mNativeMediaDrmBridge = nativeMediaDrmBridge; mResetDeviceCredentialsPending = false; mMediaDrm.setOnEventListener(new MediaDrmListener()); mMediaDrm.setPropertyString(PRIVACY_MODE, "enable"); String currentSecurityLevel = mMediaDrm.getPropertyString(SECURITY_LEVEL); Log.e(TAG, "Security level: current " + currentSecurityLevel + ", new " + securityLevel); if (!securityLevel.equals(currentSecurityLevel)) mMediaDrm.setPropertyString(SECURITY_LEVEL, securityLevel); } /** * Create a MediaCrypto object. * * @return if a MediaCrypto object is successfully created. */ private boolean createMediaCrypto() { assert(mSessionId != null); assert(mMediaCrypto == null); try { final byte[] session = mSessionId.getBytes("UTF-8"); if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) { mMediaCrypto = new MediaCrypto(mSchemeUUID, session); } } catch (android.media.MediaCryptoException e) { Log.e(TAG, "Cannot create MediaCrypto " + e.toString()); return false; } catch (java.io.UnsupportedEncodingException e) { Log.e(TAG, "Cannot create MediaCrypto " + e.toString()); return false; } assert(mMediaCrypto != null); nativeOnMediaCryptoReady(mNativeMediaDrmBridge); return true; } /** * Open a new session and return the sessionId. * * @return false if unexpected error happens. Return true if a new session * is successfully opened, or if provisioning is required to open a session. */ private boolean openSession() { assert(mSessionId == null); if (mMediaDrm == null) { return false; } try { final byte[] sessionId = mMediaDrm.openSession(); mSessionId = new String(sessionId, "UTF-8"); } catch (android.media.NotProvisionedException e) { Log.e(TAG, "Cannot open a new session: " + e.toString()); return true; } catch (Exception e) { Log.e(TAG, "Cannot open a new session: " + e.toString()); return false; } assert(mSessionId != null); return createMediaCrypto(); } /** * 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, String securityLevel, int nativeMediaDrmBridge) { UUID cryptoScheme = getUUIDFromBytes(schemeUUID); if (cryptoScheme == null || !MediaDrm.isCryptoSchemeSupported(cryptoScheme)) { return null; } MediaDrmBridge media_drm_bridge = null; try { media_drm_bridge = new MediaDrmBridge( cryptoScheme, securityLevel, nativeMediaDrmBridge); } catch (android.media.UnsupportedSchemeException e) { Log.e(TAG, "Unsupported DRM scheme: " + e.toString()); } catch (java.lang.IllegalArgumentException e) { Log.e(TAG, "Failed to create MediaDrmBridge: " + e.toString()); } catch (java.lang.IllegalStateException e) { Log.e(TAG, "Failed to create MediaDrmBridge: " + e.toString()); } return media_drm_bridge; } /** * 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() { if (mMediaCrypto != null) { mMediaCrypto.release(); mMediaCrypto = null; } if (mSessionId != null) { try { final byte[] session = mSessionId.getBytes("UTF-8"); mMediaDrm.closeSession(session); } catch (java.io.UnsupportedEncodingException e) { Log.e(TAG, "Failed to close session: " + e.toString()); } mSessionId = null; } if (mMediaDrm != null) { mMediaDrm.release(); mMediaDrm = null; } } /** * Generate a key request and post an asynchronous task to the native side * with the response message. * * @param initData Data needed to generate the key request. * @param mime Mime type. */ @CalledByNative private void generateKeyRequest(byte[] initData, String mime) { Log.d(TAG, "generateKeyRequest()."); if (mMimeType == null) { mMimeType = mime; } else if (!mMimeType.equals(mime)) { onKeyError(); return; } if (mSessionId == null) { if (!openSession()) { onKeyError(); return; } // NotProvisionedException happened during openSession(). if (mSessionId == null) { if (mPendingInitData != null) { Log.e(TAG, "generateKeyRequest called when another call is pending."); onKeyError(); return; } // We assume MediaDrm.EVENT_PROVISION_REQUIRED is always fired if // NotProvisionedException is throwed in openSession(). // generateKeyRequest() will be resumed after provisioning is finished. // TODO(xhwang): Double check if this assumption is true. Otherwise we need // to handle the exception in openSession more carefully. mPendingInitData = initData; return; } } try { final byte[] session = mSessionId.getBytes("UTF-8"); HashMap<String, String> optionalParameters = new HashMap<String, String>(); final MediaDrm.KeyRequest request = mMediaDrm.getKeyRequest( session, initData, mime, MediaDrm.KEY_TYPE_STREAMING, optionalParameters); mHandler.post(new Runnable(){ public void run() { nativeOnKeyMessage(mNativeMediaDrmBridge, mSessionId, request.getData(), request.getDefaultUrl()); } }); return; } catch (android.media.NotProvisionedException e) { // MediaDrm.EVENT_PROVISION_REQUIRED is also fired in this case. // Provisioning is handled in the handler of that event. Log.e(TAG, "Cannot get key request: " + e.toString()); return; } catch (java.io.UnsupportedEncodingException e) { Log.e(TAG, "Cannot get key request: " + e.toString()); } onKeyError(); } /** * Cancel a key request for a session Id. * * @param sessionId Crypto session Id. */ @CalledByNative private void cancelKeyRequest(String sessionId) { if (mSessionId == null || !mSessionId.equals(sessionId)) { return; } try { final byte[] session = sessionId.getBytes("UTF-8"); mMediaDrm.removeKeys(session); } catch (java.io.UnsupportedEncodingException e) { Log.e(TAG, "Cannot cancel key request: " + e.toString()); } } /** * Add a key for a session Id. * * @param sessionId Crypto session Id. * @param key Response data from the server. */ @CalledByNative private void addKey(String sessionId, byte[] key) { if (mSessionId == null || !mSessionId.equals(sessionId)) { return; } try { final byte[] session = sessionId.getBytes("UTF-8"); try { mMediaDrm.provideKeyResponse(session, 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.toString()); } mHandler.post(new Runnable() { public void run() { nativeOnKeyAdded(mNativeMediaDrmBridge, mSessionId); } }); return; } catch (android.media.NotProvisionedException e) { Log.e(TAG, "failed to provide key response: " + e.toString()); } catch (android.media.DeniedByServerException e) { Log.e(TAG, "failed to provide key response: " + e.toString()); } catch (java.io.UnsupportedEncodingException e) { Log.e(TAG, "failed to provide key response: " + e.toString()); } onKeyError(); } /** * Return the security level of this DRM object. */ @CalledByNative private String getSecurityLevel() { return mMediaDrm.getPropertyString("securityLevel"); } /** * Called when the provision response is received. * * @param response Response data from the provision server. */ private void onProvisionResponse(byte[] response) { Log.d(TAG, "onProvisionResponse()"); // 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; return; } if (!success) { onKeyError(); } } /** * 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); } catch (android.media.DeniedByServerException e) { Log.e(TAG, "failed to provide provision response: " + e.toString()); return false; } catch (java.lang.IllegalStateException e) { Log.e(TAG, "failed to provide provision response: " + e.toString()); return false; } if (mPendingInitData != null) { assert(!mResetDeviceCredentialsPending); byte[] initData = mPendingInitData; mPendingInitData = null; generateKeyRequest(initData, mMimeType); } return true; } private void onKeyError() { // TODO(qinmin): pass the error code to native. mHandler.post(new Runnable() { public void run() { nativeOnKeyError(mNativeMediaDrmBridge, mSessionId); } }); } private class MediaDrmListener implements MediaDrm.OnEventListener { @Override public void onEvent(MediaDrm mediaDrm, byte[] sessionId, int event, int extra, byte[] data) { switch(event) { case MediaDrm.EVENT_PROVISION_REQUIRED: Log.d(TAG, "MediaDrm.EVENT_PROVISION_REQUIRED."); MediaDrm.ProvisionRequest request = mMediaDrm.getProvisionRequest(); PostRequestTask postTask = new PostRequestTask(request.getData()); postTask.execute(request.getDefaultUrl()); break; case MediaDrm.EVENT_KEY_REQUIRED: Log.d(TAG, "MediaDrm.EVENT_KEY_REQUIRED."); generateKeyRequest(data, mMimeType); break; case MediaDrm.EVENT_KEY_EXPIRED: Log.d(TAG, "MediaDrm.EVENT_KEY_EXPIRED."); onKeyError(); break; case MediaDrm.EVENT_VENDOR_DEFINED: Log.d(TAG, "MediaDrm.EVENT_VENDOR_DEFINED."); assert(false); break; default: Log.e(TAG, "Invalid DRM event " + (int)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(int nativeMediaDrmBridge); private native void nativeOnKeyMessage(int nativeMediaDrmBridge, String sessionId, byte[] message, String destinationUrl); private native void nativeOnKeyAdded(int nativeMediaDrmBridge, String sessionId); private native void nativeOnKeyError(int nativeMediaDrmBridge, String sessionId); private native void nativeOnResetDeviceCredentialsCompleted( int nativeMediaDrmBridge, boolean success); }