blob: 3b881d0bbf4488cfc15ceea9a16b0302ce1e7f6d [file] [log] [blame]
// 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.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.MediaCrypto;
import android.media.MediaDrm;
import android.os.Build;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.MainDex;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.media.MediaDrmSessionManager.SessionId;
import org.chromium.media.MediaDrmSessionManager.SessionInfo;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Queue;
import java.util.UUID;
// Implementation Notes of MediaDrmBridge:
//
// MediaCrypto Creation: If requiresMediaCrypto is true, the caller is guaranteed to wait until
// MediaCrypto is created to call any other methods. A mMediaCryptoSession is opened after MediaDrm
// is created. This session will NOT be added to mSessionManager and will only be used to create the
// MediaCrypto object. createMediaCrypto() may trigger the provisioning process, where MediaCrypto
// creation will resume after provisioning completes.
//
// Unprovision: If requiresMediaCrypto is false, MediaDrmBridge is not created for playback.
// Instead, it's created to unprovision the device/origin, which is only supported on newer Android
// versions. unprovision() is triggered when user clears media licenses.
//
// NotProvisionedException: If this exception is thrown in operations other than
// createMediaCrypto(), we will fail that operation and not trying to provision again.
//
// Session Manager: Each createSession() call creates a new session. All created sessions are
// managed in mSessionManager except for mMediaCryptoSession.
//
// Error Handling: Whenever an unexpected error occurred, we'll call release() to release all
// resources immediately, clear all states and fail all pending operations. After that all calls to
// this object will fail (e.g. return null or reject the promise). All public APIs and callbacks
// should check mMediaBridge to make sure release() hasn't been called.
/**
* A wrapper of the android MediaDrm class. Each MediaDrmBridge manages multiple sessions for
* MediaCodecAudioDecoders or MediaCodecVideoDecoders.
*/
@JNINamespace("media")
@MainDex
@SuppressLint("WrongConstant")
public class MediaDrmBridge {
private static final String TAG = "media";
private static final String SECURITY_LEVEL = "securityLevel";
private static final String SERVER_CERTIFICATE = "serviceCertificate";
private static final String ORIGIN = "origin";
private static final String PRIVACY_MODE = "privacyMode";
private static final String SESSION_SHARING = "sessionSharing";
private static final String ENABLE = "enable";
private static final long INVALID_NATIVE_MEDIA_DRM_BRIDGE = 0;
private static final String FIRST_API_LEVEL = "ro.product.first_api_level";
// Scheme UUID for Widevine. See http://dashif.org/identifiers/protection/
private static final UUID WIDEVINE_UUID =
UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed");
// On Android L and before, MediaDrm doesn't support KeyStatus at all. On later Android
// versions, key IDs are not available on sessions where getKeyRequest() has been called with
// KEY_TYPE_RELEASE. In these cases, the EME spec recommends to use a one-byte key ID 0:
// "Some older platforms may contain Key System implementations that do not expose key IDs,
// making it impossible to provide a compliant user agent implementation. To maximize
// interoperability, user agent implementations exposing such CDMs should implement this member
// as follows: Whenever a non-empty list is appropriate, such as when the key session
// represented by this object may contain key(s), populate the map with a single pair containing
// the one-byte key ID 0 and the MediaKeyStatus most appropriate for the aggregated status of
// this object."
// See details: https://www.w3.org/TR/encrypted-media/#dom-mediakeysession-keystatuses
private static final byte[] DUMMY_KEY_ID = new byte[] {0};
// Special provision response to remove the cert.
private static final byte[] UNPROVISION = ApiCompatibilityUtils.getBytesUtf8("unprovision");
private MediaDrm mMediaDrm;
private MediaCrypto mMediaCrypto;
private long mNativeMediaDrmBridge;
private UUID mSchemeUUID;
private final boolean mRequiresMediaCrypto;
// A session only for the purpose of creating a MediaCrypto object. Created
// after construction, or after the provisioning process is successfully
// completed. No getKeyRequest() should be called on |mMediaCryptoSession|.
private SessionId mMediaCryptoSession;
// The map of all opened sessions (excluding mMediaCryptoSession) to their
// associated meta data, e.g. mime types, key types.
private MediaDrmSessionManager mSessionManager;
// The persistent storage to record origin provisioning informations.
private MediaDrmStorageBridge mStorage;
// Whether the current MediaDrmBridge instance is waiting for provisioning response.
private boolean mProvisioningPending;
// Current 'ORIGIN" setting.
private String mOrigin;
// Boolean to track if 'ORIGIN' is set in MediaDrm.
private boolean mOriginSet;
private SessionEventDeferrer mSessionEventDeferrer;
// Defer the creation of MediaCryptor creation. Only used when mRequiresMediaCrypto is true.
private static final MediaCryptoDeferrer sMediaCryptoDeferrer = new MediaCryptoDeferrer();
private static class MediaCryptoDeferrer {
// Whether any MediaDrmBridge instance is waiting for provisioning response.
private boolean mIsProvisioning;
// Pending events to fire after provisioning is finished.
private final Queue<Runnable> mEventHandlers;
MediaCryptoDeferrer() {
mIsProvisioning = false;
mEventHandlers = new ArrayDeque<Runnable>();
}
boolean isProvisioning() {
return mIsProvisioning;
}
void onProvisionStarted() {
assert !mIsProvisioning;
mIsProvisioning = true;
}
void defer(Runnable handler) {
assert mIsProvisioning;
mEventHandlers.add(handler);
}
void onProvisionDone() {
assert mIsProvisioning;
mIsProvisioning = false;
// This will cause createMediaCrypto() on another MediaDrmBridge object and could cause
// reentrance into the shared static sMediaCryptoDeferrer. For example, during
// createMediaCrypto(), we could hit NotProvisionedException again, and call
// isProvisioning() to check whether it can start provisioning or not. If so, it'll
// call onProvisionStarted(). To avoid the case where we call createMediaCrypto() and
// then immediately call defer(), we'll return early whenever mIsProvisioning becomes
// true.
while (!mEventHandlers.isEmpty()) {
Log.d(TAG, "run deferred CreateMediaCrypto() calls");
Runnable r = mEventHandlers.element();
mEventHandlers.remove();
r.run();
if (mIsProvisioning) {
Log.d(TAG, "provision triggerred while running deferred CreateMediaCrypto()");
return;
}
}
}
}
// Block MediaDrm event for |mSessionId|. MediaDrm may fire event before the
// functions return. This may break Chromium CDM API's assumption. For
// example, when loading session, 'restoreKeys' will trigger key status
// change event. But the session isn't known to Chromium CDM because the
// promise isn't resolved. The class can block and collect these events and
// fire these events later.
private static class SessionEventDeferrer {
private final SessionId mSessionId;
private final ArrayList<Runnable> mEventHandlers;
SessionEventDeferrer(SessionId sessionId) {
mSessionId = sessionId;
mEventHandlers = new ArrayList<>();
}
boolean shouldDefer(SessionId sessionId) {
return mSessionId.isEqual(sessionId);
}
void defer(Runnable handler) {
mEventHandlers.add(handler);
}
void fire() {
for (Runnable r : mEventHandlers) {
r.run();
}
mEventHandlers.clear();
}
}
/**
* An equivalent of MediaDrm.KeyStatus, which is only available on M+.
*/
private static class KeyStatus {
private final byte[] mKeyId;
private final int mStatusCode;
private KeyStatus(byte[] keyId, int statusCode) {
mKeyId = keyId;
mStatusCode = statusCode;
}
@CalledByNative("KeyStatus")
private byte[] getKeyId() {
return mKeyId;
}
@CalledByNative("KeyStatus")
private int getStatusCode() {
return mStatusCode;
}
}
/**
* Creates a dummy single element list of KeyStatus with a dummy key ID and
* the specified keyStatus.
*/
private static List<KeyStatus> getDummyKeysInfo(int statusCode) {
List<KeyStatus> keysInfo = new ArrayList<KeyStatus>();
keysInfo.add(new KeyStatus(DUMMY_KEY_ID, statusCode));
return keysInfo;
}
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 boolean isNativeMediaDrmBridgeValid() {
return mNativeMediaDrmBridge != INVALID_NATIVE_MEDIA_DRM_BRIDGE;
}
private boolean isWidevine() {
return mSchemeUUID.equals(WIDEVINE_UUID);
}
@TargetApi(Build.VERSION_CODES.M)
private MediaDrmBridge(UUID schemeUUID, boolean requiresMediaCrypto, long nativeMediaDrmBridge,
long nativeMediaDrmStorageBridge) throws android.media.UnsupportedSchemeException {
mSchemeUUID = schemeUUID;
mMediaDrm = new MediaDrm(schemeUUID);
mRequiresMediaCrypto = requiresMediaCrypto;
mNativeMediaDrmBridge = nativeMediaDrmBridge;
assert isNativeMediaDrmBridgeValid();
mStorage = new MediaDrmStorageBridge(nativeMediaDrmStorageBridge);
mSessionManager = new MediaDrmSessionManager(mStorage);
mProvisioningPending = false;
mMediaDrm.setOnEventListener(new EventListener());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mMediaDrm.setOnExpirationUpdateListener(new ExpirationUpdateListener(), null);
mMediaDrm.setOnKeyStatusChangeListener(new KeyStatusChangeListener(), null);
}
if (isWidevine()) {
mMediaDrm.setPropertyString(PRIVACY_MODE, ENABLE);
mMediaDrm.setPropertyString(SESSION_SHARING, ENABLE);
}
}
/**
* Create a MediaCrypto object.
*
* @return false upon fatal error in creating MediaCrypto. Returns true
* otherwise, including the following two cases:
* 1. MediaCrypto is successfully created and notified.
* 2. Device is not provisioned and MediaCrypto creation will be tried
* again after the provisioning process is completed.
*
* When false is returned, release() is called within the function, which
* will notify the native code with a null MediaCrypto, if needed.
*/
private boolean createMediaCrypto() {
assert mMediaDrm != null;
assert !mProvisioningPending;
assert mMediaCryptoSession == null;
// Open media crypto session.
byte[] mediaCryptoSessionDrmId = null;
try {
mediaCryptoSessionDrmId = openSession();
} catch (android.media.NotProvisionedException e) {
Log.d(TAG, "Not provisioned during openSession()");
if (!sMediaCryptoDeferrer.isProvisioning()) {
return startProvisioning();
}
// Cannot provision. Defer MediaCrypto creation and try again later.
Log.d(TAG, "defer CreateMediaCrypto() calls");
sMediaCryptoDeferrer.defer(new Runnable() {
@Override
public void run() {
createMediaCrypto();
}
});
return true;
}
if (mediaCryptoSessionDrmId == null) {
Log.e(TAG, "Cannot create MediaCrypto Session.");
// No need to release() here since openSession() does so on failure.
return false;
}
mMediaCryptoSession = SessionId.createTemporarySessionId(mediaCryptoSessionDrmId);
Log.d(TAG, "MediaCrypto Session created: %s", mMediaCryptoSession.toHexString());
// Create MediaCrypto object.
try {
if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) {
mMediaCrypto = new MediaCrypto(mSchemeUUID, mMediaCryptoSession.drmId());
Log.d(TAG, "MediaCrypto successfully created!");
onMediaCryptoReady(mMediaCrypto);
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 ID of the session opened. Returns null if unexpected error happened.
*/
private byte[] openSession() throws android.media.NotProvisionedException {
assert mMediaDrm != null;
try {
byte[] sessionId = mMediaDrm.openSession();
// Make a clone here in case the underlying byte[] is modified.
return sessionId.clone();
} catch (java.lang.RuntimeException e) { // TODO(xhwang): Drop this?
Log.e(TAG, "Cannot open a new session", e);
release();
return null;
} catch (android.media.NotProvisionedException e) {
// Throw NotProvisionedException so that we can startProvisioning().
throw e;
} catch (android.media.MediaDrmException e) {
// Other MediaDrmExceptions (e.g. ResourceBusyException) are not
// recoverable.
Log.e(TAG, "Cannot open a new session", e);
release();
return null;
}
}
/**
* 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.
*
* @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);
if (containerMimeType.isEmpty()) {
return MediaDrm.isCryptoSchemeSupported(cryptoScheme);
}
return MediaDrm.isCryptoSchemeSupported(cryptoScheme, containerMimeType);
}
/**
* Returns the first API level for this product.
*
* @return the converted value for FIRST_API_LEVEL if available,
* 0 otherwise.
*/
@CalledByNative
private static int getFirstApiLevel() {
int firstApiLevel = 0;
try {
final Class<?> systemProperties = Class.forName("android.os.SystemProperties");
final Method getInt = systemProperties.getMethod("getInt", String.class, int.class);
firstApiLevel = (Integer) getInt.invoke(null, FIRST_API_LEVEL, 0);
} catch (Exception e) {
Log.e(TAG, "Exception while getting system property %s. Using default.",
FIRST_API_LEVEL, e);
firstApiLevel = 0;
}
return firstApiLevel;
}
/**
* Create a new MediaDrmBridge from the crypto scheme UUID.
*
* @param schemeUUID Crypto scheme UUID.
* @param securityOrigin Security origin. Empty value means no need for origin isolated storage.
* @param securityLevel Security level. If empty, the default one should be used.
* @param nativeMediaDrmBridge Native object of this class.
* @param nativeMediaDrmStorageBridge Native object of persistent storage.
*/
@CalledByNative
private static MediaDrmBridge create(byte[] schemeUUID, String securityOrigin,
String securityLevel, boolean requiresMediaCrypto, long nativeMediaDrmBridge,
long nativeMediaDrmStorageBridge) {
Log.i(TAG, "Create MediaDrmBridge with level %s and origin %s", securityLevel,
securityOrigin);
MediaDrmBridge mediaDrmBridge = null;
try {
UUID cryptoScheme = getUUIDFromBytes(schemeUUID);
if (cryptoScheme == null || !MediaDrm.isCryptoSchemeSupported(cryptoScheme)) {
return null;
}
mediaDrmBridge = new MediaDrmBridge(cryptoScheme, requiresMediaCrypto,
nativeMediaDrmBridge, nativeMediaDrmStorageBridge);
} catch (android.media.UnsupportedSchemeException e) {
Log.e(TAG, "Unsupported DRM scheme", e);
return null;
} catch (java.lang.IllegalArgumentException e) {
Log.e(TAG, "Failed to create MediaDrmBridge", e);
return null;
} catch (java.lang.IllegalStateException e) {
Log.e(TAG, "Failed to create MediaDrmBridge", e);
return null;
}
if (!securityLevel.isEmpty() && !mediaDrmBridge.setSecurityLevel(securityLevel)) {
mediaDrmBridge.release();
return null;
}
if (!securityOrigin.isEmpty() && !mediaDrmBridge.setOrigin(securityOrigin)) {
mediaDrmBridge.release();
return null;
}
// When session support is required, we need to create MediaCrypto to
// finish the CDM creation process. This may trigger the provisioning
// process, in which case MediaCrypto will be created after provision
// is finished.
if (requiresMediaCrypto && !mediaDrmBridge.createMediaCrypto()) {
// No need to call release() as createMediaCrypto() does if it fails.
return null;
}
return mediaDrmBridge;
}
/**
* Set the security origin for the MediaDrm. All information should be isolated for different
* origins, e.g. certificates, licenses.
*/
private boolean setOrigin(String origin) {
assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
Log.d(TAG, "Set origin: %s", origin);
if (!isWidevine()) {
Log.d(TAG, "Property " + ORIGIN + " isn't supported");
return true;
}
assert mMediaDrm != null;
assert !origin.isEmpty();
try {
mMediaDrm.setPropertyString(ORIGIN, origin);
mOrigin = origin;
mOriginSet = true;
return true;
} catch (java.lang.IllegalArgumentException e) {
Log.e(TAG, "Failed to set security origin %s", origin, e);
} catch (java.lang.IllegalStateException e) {
Log.e(TAG, "Failed to set security origin %s", origin, e);
}
Log.e(TAG, "Security origin %s not supported!", origin);
return false;
}
/**
* 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.
*
* @param securityLevel Security level to be set.
* @return whether the security level was successfully set.
*/
private boolean setSecurityLevel(String securityLevel) {
if (!isWidevine()) {
Log.d(TAG, "Security level is not supported.");
return true;
}
assert mMediaDrm != null;
assert !securityLevel.isEmpty();
String currentSecurityLevel = getSecurityLevel();
if (currentSecurityLevel.equals("")) {
// Failure logged by getSecurityLevel().
return false;
}
Log.d(TAG, "Security level: current %s, new %s", currentSecurityLevel, 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) {
} catch (java.lang.IllegalStateException e) {
}
Log.e(TAG, "Security level %s not supported!", securityLevel);
return false;
}
/**
* Set the server certificate.
*
* @param certificate Server certificate to be set.
* @return whether the server certificate was successfully set.
*/
@CalledByNative
private boolean setServerCertificate(byte[] certificate) {
if (!isWidevine()) {
Log.d(TAG, "Setting server certificate is not supported.");
return true;
}
try {
mMediaDrm.setPropertyByteArray(SERVER_CERTIFICATE, certificate);
return true;
} catch (java.lang.IllegalArgumentException e) {
Log.e(TAG, "Failed to set server certificate", e);
} catch (java.lang.IllegalStateException e) {
Log.e(TAG, "Failed to set server certificate", e);
}
return false;
}
/**
* Provision the current origin. Normally provisioning will be triggered
* automatically when MediaCrypto is needed (in the constructor).
* However, this is available to preprovision an origin separately.
* MediaDrmBridgeJni.get().onProvisioningComplete() will be called indicating success/failure.
*/
@CalledByNative
private void provision() {
// This should only be called if no MediaCrypto needed.
assert mMediaDrm != null;
assert !mProvisioningPending;
assert !mRequiresMediaCrypto;
// Provision only works for origin isolated storage.
if (!mOriginSet) {
Log.e(TAG, "Calling provision() without an origin.");
MediaDrmBridgeJni.get().onProvisioningComplete(
mNativeMediaDrmBridge, MediaDrmBridge.this, false);
return;
}
// The security level used for provisioning cannot be set and is cached from when a need for
// provisioning is last detected. So if we call startProvisioning() it will use the default
// security level, which may not match the security level needed. As a result this code must
// call openSession(), which will result in the security level being cached. We don't care
// about the session, so if it opens simply close it.
try {
// This will throw a NotProvisionedException if provisioning needed. If it succeeds,
// assume this origin ID is already provisioned.
byte[] drmId = openSession();
// Provisioning is not required. If a session was actually opened, close it.
if (drmId != null) {
SessionId sessionId = SessionId.createTemporarySessionId(drmId);
closeSessionNoException(sessionId);
}
// Indicate that provisioning succeeded.
MediaDrmBridgeJni.get().onProvisioningComplete(
mNativeMediaDrmBridge, MediaDrmBridge.this, true);
} catch (android.media.NotProvisionedException e) {
if (!startProvisioning()) {
// Indicate that provisioning failed.
MediaDrmBridgeJni.get().onProvisioningComplete(
mNativeMediaDrmBridge, MediaDrmBridge.this, false);
}
}
}
/**
* Unprovision the current origin, a.k.a removing the cert for current origin.
*/
@CalledByNative
private void unprovision() {
if (mMediaDrm == null) {
return;
}
// Unprovision only works for origin isolated storage.
if (!mOriginSet) {
return;
}
provideProvisionResponse(UNPROVISION);
}
/**
* Destroy the MediaDrmBridge object.
*/
@CalledByNative
private void destroy() {
mNativeMediaDrmBridge = INVALID_NATIVE_MEDIA_DRM_BRIDGE;
if (mMediaDrm != null) {
release();
}
}
/**
* Release all allocated resources and finish all pending operations.
*/
private void release() {
// Note that mNativeMediaDrmBridge may have already been reset (see destroy()).
assert mMediaDrm != null;
// Close all open sessions.
for (SessionId sessionId : mSessionManager.getAllSessionIds()) {
try {
// Some implementations don't have removeKeys, crbug/475632
mMediaDrm.removeKeys(sessionId.drmId());
} catch (Exception e) {
Log.e(TAG, "removeKeys failed: ", e);
}
closeSessionNoException(sessionId);
onSessionClosed(sessionId);
}
mSessionManager = new MediaDrmSessionManager(mStorage);
// Close mMediaCryptoSession if it's open.
if (mMediaCryptoSession != null) {
closeSessionNoException(mMediaCryptoSession);
mMediaCryptoSession = null;
}
if (mMediaDrm != null) {
mMediaDrm.release();
mMediaDrm = null;
}
if (mMediaCrypto != null) {
mMediaCrypto.release();
mMediaCrypto = null;
} else {
// MediaCrypto never notified. Notify a null one now.
onMediaCryptoReady(null);
}
}
/**
* Get a key request.
*
* @param sessionId ID of 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.
* @param keyType Key type for the requested key.
* @param optionalParameters Optional parameters to pass to the DRM plugin.
*
* @return the key request.
*/
private MediaDrm.KeyRequest getKeyRequest(SessionId sessionId, byte[] data, String mime,
int keyType, HashMap<String, String> optionalParameters)
throws android.media.NotProvisionedException {
assert mMediaDrm != null;
assert mMediaCryptoSession != null;
assert !mProvisioningPending;
if (optionalParameters == null) {
optionalParameters = new HashMap<String, String>();
}
MediaDrm.KeyRequest request = null;
try {
byte[] scopeId =
keyType == MediaDrm.KEY_TYPE_RELEASE ? sessionId.keySetId() : sessionId.drmId();
assert scopeId != null;
request = mMediaDrm.getKeyRequest(scopeId, data, mime, keyType, optionalParameters);
} catch (java.lang.IllegalStateException e) {
// We've seen both MediaDrmStateException and MediaDrmResetException happening.
// Since both are IllegalStateExceptions, so they will be handled here.
// See b/21307186 and crbug.com/1169050 for details.
Log.e(TAG, "Failed to getKeyRequest().", e);
}
String result = (request != null) ? "successed" : "failed";
Log.d(TAG, "getKeyRequest %s!", result);
return request;
}
/**
* createSession interface to be called from native using primitive types.
* @see createSession(byte[], String, HashMap<String, String>, long)
*/
@CalledByNative
private void createSessionFromNative(byte[] initData, String mime, int keyType,
String[] optionalParamsArray, long promiseId) {
HashMap<String, String> optionalParameters = new HashMap<String, String>();
if (optionalParamsArray != null) {
if (optionalParamsArray.length % 2 != 0) {
throw new IllegalArgumentException(
"Additional data array doesn't have equal keys/values");
}
for (int i = 0; i < optionalParamsArray.length; i += 2) {
optionalParameters.put(optionalParamsArray[i], optionalParamsArray[i + 1]);
}
}
createSession(initData, mime, keyType, optionalParameters, promiseId);
}
/**
* Create a session, and generate a request with |initData| and |mime|.
*
* @param initData Data needed to generate the key request.
* @param mime Mime type.
* @param keyType Key type.
* @param optionalParameters Additional data to pass to getKeyRequest.
* @param promiseId Promise ID for this call.
*/
private void createSession(byte[] initData, String mime, int keyType,
HashMap<String, String> optionalParameters, long promiseId) {
Log.d(TAG, "createSession()");
if (mMediaDrm == null) {
Log.e(TAG, "createSession() called when MediaDrm is null.");
onPromiseRejected(promiseId, "MediaDrm released previously.");
return;
}
assert mMediaCryptoSession != null;
assert !mProvisioningPending;
boolean newSessionOpened = false;
SessionId sessionId = null;
try {
byte[] drmId = openSession();
if (drmId == null) {
onPromiseRejected(promiseId, "Open session failed.");
return;
}
newSessionOpened = true;
assert keyType == MediaDrm.KEY_TYPE_STREAMING || keyType == MediaDrm.KEY_TYPE_OFFLINE;
sessionId = (keyType == MediaDrm.KEY_TYPE_OFFLINE)
? SessionId.createPersistentSessionId(drmId)
: SessionId.createTemporarySessionId(drmId);
MediaDrm.KeyRequest request =
getKeyRequest(sessionId, initData, mime, keyType, optionalParameters);
if (request == null) {
closeSessionNoException(sessionId);
onPromiseRejected(promiseId, "Generate request failed.");
return;
}
// Success!
Log.d(TAG, "createSession(): Session (%s) created.", sessionId.toHexString());
onPromiseResolvedWithSession(promiseId, sessionId);
onSessionMessage(sessionId, request);
mSessionManager.put(sessionId, mime, keyType);
} catch (android.media.NotProvisionedException e) {
Log.e(TAG, "Device not provisioned", e);
if (newSessionOpened) {
closeSessionNoException(sessionId);
}
onPromiseRejected(promiseId, "Device not provisioned during createSession().");
}
}
/**
* Search and return the SessionId for raw EME/DRM session id.
*
* @param emeId Raw EME session Id.
* @return SessionId of |emeId| if exists and isn't a MediaCryptoSession, null otherwise.
*/
private SessionId getSessionIdByEmeId(byte[] emeId) {
if (mMediaCryptoSession == null) {
Log.e(TAG, "Session doesn't exist because media crypto session is not created.");
return null;
}
SessionId sessionId = mSessionManager.getSessionIdByEmeId(emeId);
if (sessionId == null) {
return null;
}
assert !mMediaCryptoSession.isEqual(sessionId);
return sessionId;
}
/**
* Similar with getSessionIdByEmeId, just search for raw DRM session id.
*/
private SessionId getSessionIdByDrmId(byte[] drmId) {
if (mMediaCryptoSession == null) {
Log.e(TAG, "Session doesn't exist because media crypto session is not created.");
return null;
}
SessionId sessionId = mSessionManager.getSessionIdByDrmId(drmId);
if (sessionId == null) {
return null;
}
assert !mMediaCryptoSession.isEqual(sessionId);
return sessionId;
}
/**
* Close a session that was previously created by createSession().
*
* @param emeSessionId ID of session to be closed.
* @param promiseId Promise ID of this call.
*/
@CalledByNative
private void closeSession(byte[] emeSessionId, long promiseId) {
Log.d(TAG, "closeSession()");
if (mMediaDrm == null) {
onPromiseRejected(promiseId, "closeSession() called when MediaDrm is null.");
return;
}
SessionId sessionId = getSessionIdByEmeId(emeSessionId);
if (sessionId == null) {
onPromiseRejected(promiseId,
"Invalid sessionId in closeSession(): " + SessionId.toHexString(emeSessionId));
return;
}
try {
// Some implementations don't have removeKeys, crbug/475632
mMediaDrm.removeKeys(sessionId.drmId());
} catch (Exception e) {
Log.e(TAG, "removeKeys failed: ", e);
}
closeSessionNoException(sessionId);
mSessionManager.remove(sessionId);
onPromiseResolved(promiseId);
onSessionClosed(sessionId);
Log.d(TAG, "Session %s closed", sessionId.toHexString());
}
/**
* Close the session without worry about the exception, because some
* implementations let this method throw exception, crbug/611865.
*/
private void closeSessionNoException(SessionId sessionId) {
try {
mMediaDrm.closeSession(sessionId.drmId());
} catch (Exception e) {
Log.e(TAG, "closeSession failed: ", e);
}
}
/**
* Update a session with response.
*
* @param emeSessionId Reference ID of session to be updated.
* @param response Response data from the server.
* @param promiseId Promise ID of this call.
*/
@CalledByNative
private void updateSession(byte[] emeSessionId, byte[] response, final long promiseId) {
Log.d(TAG, "updateSession()");
if (mMediaDrm == null) {
onPromiseRejected(promiseId, "updateSession() called when MediaDrm is null.");
return;
}
final SessionId sessionId = getSessionIdByEmeId(emeSessionId);
if (sessionId == null) {
assert false; // Should never happen.
onPromiseRejected(promiseId,
"Invalid session in updateSession: " + SessionId.toHexString(emeSessionId));
return;
}
try {
SessionInfo sessionInfo = mSessionManager.get(sessionId);
boolean isKeyRelease = sessionInfo.keyType() == MediaDrm.KEY_TYPE_RELEASE;
byte[] keySetId = null;
if (isKeyRelease) {
Log.d(TAG, "updateSession() for key release");
assert sessionId.keySetId() != null;
mMediaDrm.provideKeyResponse(sessionId.keySetId(), response);
} else {
keySetId = mMediaDrm.provideKeyResponse(sessionId.drmId(), response);
}
KeyUpdatedCallback cb = new KeyUpdatedCallback(sessionId, promiseId, isKeyRelease);
if (isKeyRelease) {
mSessionManager.clearPersistentSessionInfo(sessionId, cb);
} else if (sessionInfo.keyType() == MediaDrm.KEY_TYPE_OFFLINE && keySetId != null
&& keySetId.length > 0) {
mSessionManager.setKeySetId(sessionId, keySetId, cb);
} else {
// This can be either temporary license update or server certificate update.
cb.onResult(true);
}
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);
} catch (java.lang.IllegalStateException e) {
Log.e(TAG, "failed to provide key response", e);
}
onPromiseRejected(promiseId, "Update session failed.");
release();
}
/**
* Load persistent license from storage.
*/
@CalledByNative
@TargetApi(Build.VERSION_CODES.M)
private void loadSession(byte[] emeId, final long promiseId) {
Log.d(TAG, "loadSession()");
assert !mProvisioningPending;
mSessionManager.load(emeId, new Callback<SessionId>() {
@Override
public void onResult(SessionId sessionId) {
if (sessionId == null) {
onPersistentLicenseNoExist(promiseId);
return;
}
loadSessionWithLoadedStorage(sessionId, promiseId);
}
});
}
/**
* Load session back to memory with MediaDrm. Load persistent storage
* before calling this. It will fail if persistent storage isn't loaded.
*/
@TargetApi(Build.VERSION_CODES.M)
private void loadSessionWithLoadedStorage(SessionId sessionId, final long promiseId) {
byte[] drmId = null;
try {
drmId = openSession();
if (drmId == null) {
onPromiseRejected(promiseId, "Failed to open session to load license.");
return;
}
mSessionManager.setDrmId(sessionId, drmId);
assert Arrays.equals(sessionId.drmId(), drmId);
SessionInfo sessionInfo = mSessionManager.get(sessionId);
// If persistent license (KEY_TYPE_OFFLINE) is released but we don't receive the ack
// from the server, we should avoid restoring the keys. Report success to JS so that
// they can release it again.
if (sessionInfo.keyType() == MediaDrm.KEY_TYPE_RELEASE) {
Log.w(TAG, "Persistent license is waiting for release ack.");
onPromiseResolvedWithSession(promiseId, sessionId);
// Report keystatuseschange event to JS. Ideally we should report the event with
// list of known key IDs. However we can't get the key IDs from MediaDrm. Just
// report with dummy key IDs.
onSessionKeysChange(sessionId,
getDummyKeysInfo(MediaDrm.KeyStatus.STATUS_EXPIRED).toArray(),
false /* hasAdditionalUsableKey */, true /* isKeyRelease */);
return;
}
assert sessionInfo.keyType() == MediaDrm.KEY_TYPE_OFFLINE;
// Defer event handlers until license is loaded.
assert mSessionEventDeferrer == null;
mSessionEventDeferrer = new SessionEventDeferrer(sessionId);
assert sessionId.keySetId() != null;
mMediaDrm.restoreKeys(sessionId.drmId(), sessionId.keySetId());
onPromiseResolvedWithSession(promiseId, sessionId);
mSessionEventDeferrer.fire();
mSessionEventDeferrer = null;
} catch (android.media.NotProvisionedException e) {
// If device isn't provisioned, storage loading should fail.
Log.w(TAG, "Persistent license load fail because origin isn't provisioned.");
onPersistentLicenseLoadFail(sessionId, promiseId);
} catch (java.lang.IllegalStateException e) {
assert sessionId.drmId() != null;
onPersistentLicenseLoadFail(sessionId, promiseId);
}
}
private void onPersistentLicenseNoExist(long promiseId) {
// Chromium CDM API requires resolve the promise with empty session id for non-exist
// license. See media/base/content_decryption_module.h LoadSession for more details.
onPromiseResolvedWithSession(promiseId, SessionId.createNoExistSessionId());
}
// If persistent license load fails, we want to clean the storage and report it to JS as license
// doesn't exist.
private void onPersistentLicenseLoadFail(SessionId sessionId, final long promiseId) {
closeSessionNoException(sessionId);
mSessionManager.clearPersistentSessionInfo(sessionId, new Callback<Boolean>() {
@Override
public void onResult(Boolean success) {
if (!success) {
Log.w(TAG, "Failed to clear persistent storage for non-exist license");
}
onPersistentLicenseNoExist(promiseId);
}
});
}
/**
* Remove session from device. This will mark the key as released and
* generate a key release request. The license is removed from the device
* when the session is updated with a license release response.
*/
@CalledByNative
private void removeSession(byte[] emeId, final long promiseId) {
Log.d(TAG, "removeSession()");
SessionId sessionId = getSessionIdByEmeId(emeId);
if (sessionId == null) {
onPromiseRejected(promiseId, "Session doesn't exist");
return;
}
final SessionInfo sessionInfo = mSessionManager.get(sessionId);
if (sessionInfo.keyType() == MediaDrm.KEY_TYPE_STREAMING) {
// TODO(yucliu): Support 'remove' of temporary session.
onPromiseRejected(promiseId, "Removing temporary session isn't implemented");
return;
}
assert sessionId.keySetId() != null;
// Persist the key type before removing the keys completely.
// 1. If we fails to persist the key type, both the persistent storage and MediaDrm think
// the keys are alive. JS can just remove the session again.
// 2. If we are able to persist the key type but don't get the callback, persistent storage
// thinks keys are removed but MediaDrm thinks keys are alive. JS thinks keys are removed
// next time it loads the keys, which matches the expectation of this function.
mSessionManager.setKeyType(sessionId, MediaDrm.KEY_TYPE_RELEASE, new Callback<Boolean>() {
@Override
public void onResult(Boolean success) {
if (!success) {
onPromiseRejected(promiseId, "Fail to update persistent storage");
return;
}
doRemoveSession(sessionId, sessionInfo.mimeType(), promiseId);
}
});
}
private void doRemoveSession(SessionId sessionId, String mimeType, long promiseId) {
try {
// Get key release request.
MediaDrm.KeyRequest request =
getKeyRequest(sessionId, null, mimeType, MediaDrm.KEY_TYPE_RELEASE, null);
if (request == null) {
onPromiseRejected(promiseId, "Fail to generate key release request");
return;
}
// According to EME spec:
// https://www.w3.org/TR/encrypted-media/#dom-mediakeysession-remove
// 5.5 ... run the Queue a "message" Event ...
// 5.6 Resolve promise
// Since event is queued, JS will receive event after promise is
// resolved. So resolve the promise before firing the event here.
onPromiseResolved(promiseId);
onSessionMessage(sessionId, request);
} catch (android.media.NotProvisionedException e) {
Log.e(TAG, "removeSession called on unprovisioned device");
onPromiseRejected(promiseId, "Unknown failure");
}
}
/**
* Return the security level of this DRM object. In case of failure this
* returns the empty string, which is treated by the native side as
* "DEFAULT".
* TODO(jrummell): Revisit this in the future if the security level gets
* used for more things.
*/
@CalledByNative
private String getSecurityLevel() {
if (mMediaDrm == null || !isWidevine()) {
Log.e(TAG, "getSecurityLevel(): MediaDrm is null or security level is not supported.");
return "";
}
// Any failure in getPropertyString() means we don't know what the current security level
// is.
try {
return mMediaDrm.getPropertyString(SECURITY_LEVEL);
} catch (java.lang.IllegalStateException e) {
// getPropertyString() may fail with android.media.MediaDrmResetException or
// android.media.MediaDrm.MediaDrmStateException. As MediaDrmStateException was added in
// API 21, we can't use it directly. However, both of these are IllegalStateExceptions,
// so both will be handled here.
Log.e(TAG, "Failed to get current security level", e);
return "";
} catch (Exception e) {
// getPropertyString() has been failing with android.media.ResourceBusyException on some
// devices. ResourceBusyException is not mentioned as a possible exception nor a runtime
// exception and thus can not be listed, so catching all exceptions to handle it here.
Log.e(TAG, "Failed to get current security level", e);
return "";
}
}
/**
* Start provisioning. Returns true if a provisioning request can be
* generated and has been forwarded to C++ code for handling, false
* otherwise.
*/
private boolean startProvisioning() {
Log.d(TAG, "startProvisioning");
assert !mProvisioningPending;
mProvisioningPending = true;
assert mMediaDrm != null;
if (!isNativeMediaDrmBridgeValid()) {
return false;
}
if (mRequiresMediaCrypto) {
sMediaCryptoDeferrer.onProvisionStarted();
}
// getProvisionRequest() may fail with android.media.MediaDrm.MediaDrmStateException or
// android.media.MediaDrmResetException, both of which extend IllegalStateException. As
// these specific exceptions are only available in API 21 and 23 respectively, using the
// base exception so that this will work for all API versions.
MediaDrm.ProvisionRequest request;
try {
request = mMediaDrm.getProvisionRequest();
} catch (java.lang.IllegalStateException e) {
Log.e(TAG, "Failed to get provisioning request", e);
return false;
}
Log.i(TAG, "Provisioning origin ID %s", mOriginSet ? mOrigin : "<none>");
MediaDrmBridgeJni.get().onProvisionRequest(mNativeMediaDrmBridge, MediaDrmBridge.this,
request.getDefaultUrl(), request.getData());
return true;
}
/**
* Called when the provision response is received.
*
* @param isResponseReceived Flag set to true if communication with
* provision server was successful.
* @param response Response data from the provision server.
*/
@CalledByNative
private void processProvisionResponse(boolean isResponseReceived, byte[] response) {
Log.d(TAG, "processProvisionResponse()");
assert mMediaCryptoSession == null;
assert mProvisioningPending;
mProvisioningPending = false;
boolean success = false;
// If |mMediaDrm| is released, there is no need to callback native.
if (mMediaDrm != null) {
success = isResponseReceived ? provideProvisionResponse(response) : false;
}
// This may call release() internally. However, sMediaCryptoDeferrer.onProvisionDone() will
// still be called below to ensure provisioning failure here doesn't block other
// MediaDrmBridge instances from proceeding.
onProvisioned(success);
if (mRequiresMediaCrypto) {
sMediaCryptoDeferrer.onProvisionDone();
}
}
/**
* Provides the provision 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;
}
/*
* Provisioning complete. Continue to createMediaCrypto() if required.
*
* @param success Whether provisioning has succeeded or not.
*/
void onProvisioned(boolean success) {
if (!mRequiresMediaCrypto) {
// No MediaCrypto required, so notify provisioning complete.
MediaDrmBridgeJni.get().onProvisioningComplete(
mNativeMediaDrmBridge, MediaDrmBridge.this, success);
if (!success) {
release();
}
return;
}
if (!success) {
release();
return;
}
if (!mOriginSet) {
createMediaCrypto();
return;
}
// When |mOriginSet|, notify the storage onProvisioned, and continue
// creating MediaCrypto after that.
mStorage.onProvisioned(new Callback<Boolean>() {
@Override
public void onResult(Boolean initSuccess) {
assert mMediaCryptoSession == null;
if (!initSuccess) {
Log.e(TAG, "Failed to initialize storage for origin");
release();
return;
}
createMediaCrypto();
}
});
}
/**
* Delay session event handler if |mSessionEventDeferrer| exists and
* matches |sessionId|. Otherwise run the handler immediately.
*/
private void deferEventHandleIfNeeded(SessionId sessionId, Runnable handler) {
if (mSessionEventDeferrer != null && mSessionEventDeferrer.shouldDefer(sessionId)) {
mSessionEventDeferrer.defer(handler);
return;
}
handler.run();
}
// Helper functions to make native calls.
private void onMediaCryptoReady(MediaCrypto mediaCrypto) {
if (isNativeMediaDrmBridgeValid()) {
MediaDrmBridgeJni.get().onMediaCryptoReady(
mNativeMediaDrmBridge, MediaDrmBridge.this, mediaCrypto);
}
}
private void onPromiseResolved(final long promiseId) {
if (isNativeMediaDrmBridgeValid()) {
MediaDrmBridgeJni.get().onPromiseResolved(
mNativeMediaDrmBridge, MediaDrmBridge.this, promiseId);
}
}
private void onPromiseResolvedWithSession(final long promiseId, final SessionId sessionId) {
if (isNativeMediaDrmBridgeValid()) {
MediaDrmBridgeJni.get().onPromiseResolvedWithSession(
mNativeMediaDrmBridge, MediaDrmBridge.this, promiseId, sessionId.emeId());
}
}
private void onPromiseRejected(final long promiseId, final String errorMessage) {
Log.e(TAG, "onPromiseRejected: %s", errorMessage);
if (isNativeMediaDrmBridgeValid()) {
MediaDrmBridgeJni.get().onPromiseRejected(
mNativeMediaDrmBridge, MediaDrmBridge.this, promiseId, errorMessage);
}
}
@TargetApi(Build.VERSION_CODES.M)
private void onSessionMessage(final SessionId sessionId, final MediaDrm.KeyRequest request) {
if (!isNativeMediaDrmBridgeValid()) return;
int requestType = MediaDrm.KeyRequest.REQUEST_TYPE_INITIAL;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestType = request.getRequestType();
} else {
// Prior to M, getRequestType() is not supported. Do our best guess here: Assume
// requests with a URL are renewals and all others are initial requests.
requestType = request.getDefaultUrl().isEmpty()
? MediaDrm.KeyRequest.REQUEST_TYPE_INITIAL
: MediaDrm.KeyRequest.REQUEST_TYPE_RENEWAL;
}
MediaDrmBridgeJni.get().onSessionMessage(mNativeMediaDrmBridge, MediaDrmBridge.this,
sessionId.emeId(), requestType, request.getData());
}
private void onSessionClosed(final SessionId sessionId) {
if (isNativeMediaDrmBridgeValid()) {
MediaDrmBridgeJni.get().onSessionClosed(
mNativeMediaDrmBridge, MediaDrmBridge.this, sessionId.emeId());
}
}
private void onSessionKeysChange(final SessionId sessionId, final Object[] keysInfo,
final boolean hasAdditionalUsableKey, final boolean isKeyRelease) {
if (isNativeMediaDrmBridgeValid()) {
MediaDrmBridgeJni.get().onSessionKeysChange(mNativeMediaDrmBridge, MediaDrmBridge.this,
sessionId.emeId(), keysInfo, hasAdditionalUsableKey, isKeyRelease);
}
}
private void onSessionExpirationUpdate(final SessionId sessionId, final long expirationTime) {
if (isNativeMediaDrmBridgeValid()) {
MediaDrmBridgeJni.get().onSessionExpirationUpdate(
mNativeMediaDrmBridge, MediaDrmBridge.this, sessionId.emeId(), expirationTime);
}
}
private class EventListener implements MediaDrm.OnEventListener {
@Override
public void onEvent(
MediaDrm mediaDrm, byte[] drmSessionId, int event, int extra, byte[] data) {
if (drmSessionId == null) {
Log.e(TAG, "EventListener: No session for event %d.", event);
return;
}
SessionId sessionId = getSessionIdByDrmId(drmSessionId);
if (sessionId == null) {
Log.e(TAG, "EventListener: Invalid session %s",
SessionId.toHexString(drmSessionId));
return;
}
SessionInfo sessionInfo = mSessionManager.get(sessionId);
switch (event) {
case MediaDrm.EVENT_KEY_REQUIRED:
Log.d(TAG, "MediaDrm.EVENT_KEY_REQUIRED");
MediaDrm.KeyRequest request = null;
try {
request = getKeyRequest(sessionId, data, sessionInfo.mimeType(),
sessionInfo.keyType(), null);
} catch (android.media.NotProvisionedException e) {
Log.e(TAG, "Device not provisioned", e);
return;
}
if (request != null) {
onSessionMessage(sessionId, request);
} else {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
onSessionKeysChange(sessionId,
getDummyKeysInfo(MediaDrm.KeyStatus.STATUS_INTERNAL_ERROR)
.toArray(),
false, false);
}
Log.e(TAG, "EventListener: getKeyRequest failed.");
return;
}
break;
case MediaDrm.EVENT_KEY_EXPIRED:
Log.d(TAG, "MediaDrm.EVENT_KEY_EXPIRED");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
onSessionKeysChange(sessionId,
getDummyKeysInfo(MediaDrm.KeyStatus.STATUS_EXPIRED).toArray(),
false, sessionInfo.keyType() == MediaDrm.KEY_TYPE_RELEASE);
}
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;
}
}
}
@TargetApi(Build.VERSION_CODES.M)
private class KeyStatusChangeListener implements MediaDrm.OnKeyStatusChangeListener {
private List<KeyStatus> getKeysInfo(List<MediaDrm.KeyStatus> keyInformation) {
List<KeyStatus> keysInfo = new ArrayList<KeyStatus>();
for (MediaDrm.KeyStatus keyStatus : keyInformation) {
keysInfo.add(new KeyStatus(keyStatus.getKeyId(), keyStatus.getStatusCode()));
}
return keysInfo;
}
@Override
public void onKeyStatusChange(MediaDrm md, byte[] drmSessionId,
final List<MediaDrm.KeyStatus> keyInformation, final boolean hasNewUsableKey) {
final SessionId sessionId = getSessionIdByDrmId(drmSessionId);
assert sessionId != null;
assert mSessionManager.get(sessionId) != null;
final boolean isKeyRelease =
mSessionManager.get(sessionId).keyType() == MediaDrm.KEY_TYPE_RELEASE;
deferEventHandleIfNeeded(sessionId, new Runnable() {
@Override
public void run() {
Log.d(TAG,
"KeysStatusChange: " + sessionId.toHexString() + ", "
+ hasNewUsableKey);
onSessionKeysChange(sessionId, getKeysInfo(keyInformation).toArray(),
hasNewUsableKey, isKeyRelease);
}
});
}
}
@TargetApi(Build.VERSION_CODES.M)
private class ExpirationUpdateListener implements MediaDrm.OnExpirationUpdateListener {
@Override
public void onExpirationUpdate(
MediaDrm md, byte[] drmSessionId, final long expirationTime) {
final SessionId sessionId = getSessionIdByDrmId(drmSessionId);
assert sessionId != null;
deferEventHandleIfNeeded(sessionId, new Runnable() {
@Override
public void run() {
Log.d(TAG,
"ExpirationUpdate: " + sessionId.toHexString() + ", " + expirationTime);
onSessionExpirationUpdate(sessionId, expirationTime);
}
});
}
}
private class KeyUpdatedCallback implements Callback<Boolean> {
private final SessionId mSessionId;
private final long mPromiseId;
private final boolean mIsKeyRelease;
KeyUpdatedCallback(SessionId sessionId, long promiseId, boolean isKeyRelease) {
mSessionId = sessionId;
mPromiseId = promiseId;
mIsKeyRelease = isKeyRelease;
}
@Override
public void onResult(Boolean success) {
if (!success) {
onPromiseRejected(mPromiseId, "failed to update key after response accepted");
return;
}
Log.d(TAG, "Key successfully %s for session %s", mIsKeyRelease ? "released" : "added",
mSessionId.toHexString());
onPromiseResolved(mPromiseId);
if (!mIsKeyRelease && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
onSessionKeysChange(mSessionId,
getDummyKeysInfo(MediaDrm.KeyStatus.STATUS_USABLE).toArray(), true,
mIsKeyRelease);
}
}
}
// At the native side, must post the task immediately to avoid reentrancy issues.
@NativeMethods
interface Natives {
void onMediaCryptoReady(
long nativeMediaDrmBridge, MediaDrmBridge caller, MediaCrypto mediaCrypto);
void onProvisionRequest(long nativeMediaDrmBridge, MediaDrmBridge caller, String defaultUrl,
byte[] requestData);
void onProvisioningComplete(
long nativeMediaDrmBridge, MediaDrmBridge caller, boolean success);
void onPromiseResolved(long nativeMediaDrmBridge, MediaDrmBridge caller, long promiseId);
void onPromiseResolvedWithSession(long nativeMediaDrmBridge, MediaDrmBridge caller,
long promiseId, byte[] emeSessionId);
void onPromiseRejected(long nativeMediaDrmBridge, MediaDrmBridge caller, long promiseId,
String errorMessage);
void onSessionMessage(long nativeMediaDrmBridge, MediaDrmBridge caller, byte[] emeSessionId,
int requestType, byte[] message);
void onSessionClosed(long nativeMediaDrmBridge, MediaDrmBridge caller, byte[] emeSessionId);
void onSessionKeysChange(long nativeMediaDrmBridge, MediaDrmBridge caller,
byte[] emeSessionId, Object[] keysInfo, boolean hasAdditionalUsableKey,
boolean isKeyRelease);
void onSessionExpirationUpdate(long nativeMediaDrmBridge, MediaDrmBridge caller,
byte[] emeSessionId, long expirationTime);
}
}