892 lines
33 KiB
Java
Executable File
892 lines
33 KiB
Java
Executable File
// 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);
|
|
}
|