// 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 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 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, int nativeMediaDrmBridge) {
try {
mSchemeUUID = schemeUUID;
mMediaDrm = new MediaDrm(schemeUUID);
mNativeMediaDrmBridge = nativeMediaDrmBridge;
mMediaDrm.setPropertyString("privacyMode", "enable");
mMediaDrm.setOnEventListener(new MediaDrmListener());
mHandler = new Handler();
} catch (android.media.UnsupportedSchemeException e) {
Log.e(TAG, "Unsupported DRM scheme " + e.toString());
}
}
/**
* Open a new session and return the sessionId.
*
* @return ID of the session.
*/
private String openSession() {
String session = null;
try {
final byte[] sessionId = mMediaDrm.openSession();
session = new String(sessionId, "UTF-8");
} catch (android.media.NotProvisionedException e) {
Log.e(TAG, "Cannot open a new session " + e.toString());
} catch (java.io.UnsupportedEncodingException e) {
Log.e(TAG, "Cannot open a new session " + e.toString());
}
return session;
}
/**
* Create a new MediaDrmBridge from the crypto scheme UUID.
*
* @param schemeUUID Crypto scheme UUID.
* @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 new MediaDrmBridge(cryptoScheme, nativeMediaDrmBridge);
}
return null;
}
/**
* Create a new MediaCrypto object from the session Id.
*
* @param sessionId Crypto session Id.
*/
@CalledByNative
private MediaCrypto getMediaCrypto() {
if (mMediaCrypto != null) {
return mMediaCrypto;
}
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());
} catch (java.io.UnsupportedEncodingException e) {
Log.e(TAG, "Cannot create MediaCrypto " + e.toString());
}
return mMediaCrypto;
}
/**
* Release the MediaDrmBridge object.
*/
@CalledByNative
private void release() {
if (mMediaCrypto != null) {
mMediaCrypto.release();
}
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());
}
}
mMediaDrm.release();
}
/**
* 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) {
mSessionId = openSession();
if (mSessionId == null) {
if (mPendingInitData != null) {
Log.e(TAG, "generateKeyRequest is called when another call is pending.");
onKeyError();
return;
}
// We assume some event will be fired if openSession() failed.
// 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, "provide key response.");
if (response == null || response.length == 0) {
Log.e(TAG, "Invalid provision response.");
onKeyError();
return;
}
try {
mMediaDrm.provideProvisionResponse(response);
} catch (android.media.DeniedByServerException e) {
Log.e(TAG, "failed to provide provision response " + e.toString());
onKeyError();
return;
}
if (mPendingInitData != null) {
byte[] initData = mPendingInitData;
mPendingInitData = null;
generateKeyRequest(initData, mMimeType);
}
}
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:
generateKeyRequest(data, mMimeType);
break;
case MediaDrm.EVENT_KEY_EXPIRED:
onKeyError();
break;
case 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 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);
}