new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/GeckoMediaDrmBridgeV21.java
@@ -0,0 +1,627 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.media;
+
+import java.lang.*;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.UUID;
+import java.util.ArrayDeque;
+
+import android.annotation.SuppressLint;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.media.MediaCrypto;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.MediaDrmException;
+import android.util.Log;
+
+public class GeckoMediaDrmBridgeV21 implements GeckoMediaDrm {
+ private static final String LOGTAG = "GeckoMediaDrmBridgeV21";
+ private static final String INVALID_SESSION_ID = "Invalid";
+ private static final String WIDEVINE_KEY_SYSTEM = "com.widevine.alpha";
+ private static final boolean DEBUG = false;
+ private static final UUID WIDEVINE_SCHEME_UUID =
+ new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL);
+ // MediaDrm.KeyStatus information listener is supported on M+, adding a
+ // dummy key id to report key status.
+ private static final byte[] DUMMY_KEY_ID = new byte[] {0};
+
+ private UUID mSchemeUUID;
+ private Handler mHandler;
+ private HandlerThread mHandlerThread;
+ private ByteBuffer mCryptoSessionId;
+
+ // mProvisioningPromiseId is great than 0 only during provisioning.
+ private int mProvisioningPromiseId;
+ private HashSet<ByteBuffer> mSessionIds;
+ private HashMap<ByteBuffer, String> mSessionMIMETypes;
+ private ArrayDeque<PendingCreateSessionData> mPendingCreateSessionDataQueue;
+ private GeckoMediaDrm.Callbacks mCallbacks;
+
+ private MediaCrypto mCrypto;
+ protected MediaDrm mDrm;
+
+ public static int LICENSE_REQUEST_INITIAL = 0; /*MediaKeyMessageType::License_request*/
+ public static int LICENSE_REQUEST_RENEWAL = 1; /*MediaKeyMessageType::License_renewal*/
+ public static int LICENSE_REQUEST_RELEASE = 2; /*MediaKeyMessageType::License_release*/
+
+ // Store session data while provisioning
+ private static class PendingCreateSessionData {
+ public final int mToken;
+ public final int mPromiseId;
+ public final byte[] mInitData;
+ public final String mMimeType;
+
+ private PendingCreateSessionData(int token, int promiseId,
+ byte[] initData, String mimeType) {
+ mToken = token;
+ mPromiseId = promiseId;
+ mInitData = initData;
+ mMimeType = mimeType;
+ }
+ }
+
+ public boolean isSecureDecoderComonentRequired(String mimeType) {
+ if (mCrypto != null) {
+ return mCrypto.requiresSecureDecoderComponent(mimeType);
+ }
+ return false;
+ }
+
+ private static void assertTrue(boolean condition) {
+ if (DEBUG && !condition) {
+ throw new AssertionError("Expected condition to be true");
+ }
+ }
+
+ @SuppressLint("WrongConstant")
+ private void configureVendorSpecificProperty() {
+ assertTrue(mDrm != null);
+ // Support L3 for now
+ mDrm.setPropertyString("securityLevel", "L3");
+ // Refer to chromium, set multi-session mode for Widevine.
+ if (mSchemeUUID.equals(WIDEVINE_SCHEME_UUID)) {
+ mDrm.setPropertyString("sessionSharing", "enable");
+ }
+ }
+
+ GeckoMediaDrmBridgeV21(String keySystem) throws Exception {
+ if (DEBUG) Log.d(LOGTAG, "GeckoMediaDrmBridgeV21()");
+
+ mProvisioningPromiseId = 0;
+ mSessionIds = new HashSet<ByteBuffer>();
+ mSessionMIMETypes = new HashMap<ByteBuffer, String>();
+ mPendingCreateSessionDataQueue = new ArrayDeque<PendingCreateSessionData>();
+
+ mSchemeUUID = convertKeySystemToSchemeUUID(keySystem);
+ mCryptoSessionId = null;
+
+ if (DEBUG) Log.d(LOGTAG, "mSchemeUUID : " + mSchemeUUID.toString());
+
+ // The caller of GeckoMediaDrmBridgeV21 ctor should handle exceptions
+ // threw by the following steps.
+ mDrm = new MediaDrm(mSchemeUUID);
+ configureVendorSpecificProperty();
+ mDrm.setOnEventListener(new MediaDrmListener());
+ }
+
+ @Override
+ public void setCallbacks(GeckoMediaDrm.Callbacks callbacks) {
+ assertTrue(callbacks != null);
+ mCallbacks = callbacks;
+ }
+
+ @Override
+ public void createSession(int createSessionToken,
+ int promiseId,
+ String initDataType,
+ byte[] initData) {
+ if (DEBUG) Log.d(LOGTAG, "createSession()");
+ if (mDrm == null) {
+ onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
+ return;
+ }
+
+ if (mProvisioningPromiseId > 0 && mCrypto == null) {
+ if (DEBUG) Log.d(LOGTAG, "Pending createSession because it's provisioning !");
+ savePendingCreateSessionData(createSessionToken, promiseId,
+ initData, initDataType);
+ return;
+ }
+
+ ByteBuffer sessionId = null;
+ String strSessionId = null;
+ try {
+ boolean hasMediaCrypto = ensureMediaCryptoCreated();
+ if (!hasMediaCrypto) {
+ onRejectPromise(promiseId, "MediaCrypto intance is not created !");
+ return;
+ }
+
+ sessionId = openSession();
+ if (sessionId == null) {
+ onRejectPromise(promiseId, "Cannot get a session id from MediaDrm !");
+ return;
+ }
+
+ MediaDrm.KeyRequest request = getKeyRequest(sessionId, initData, initDataType);
+ if (request == null) {
+ mDrm.closeSession(sessionId.array());
+ onRejectPromise(promiseId, "Cannot get a key request from MediaDrm !");
+ return;
+ }
+ onSessionCreated(createSessionToken,
+ promiseId,
+ sessionId.array(),
+ request.getData());
+ onSessionMessage(sessionId.array(),
+ LICENSE_REQUEST_INITIAL,
+ request.getData());
+ mSessionMIMETypes.put(sessionId, initDataType);
+ strSessionId = new String(sessionId.array());
+ mSessionIds.add(sessionId);
+ if (DEBUG) Log.d(LOGTAG, " StringID : " + strSessionId + " is put into mSessionIds ");
+ } catch (android.media.NotProvisionedException e) {
+ if (DEBUG) Log.d(LOGTAG, "Device not provisioned:" + e.getMessage());
+ if (sessionId != null) {
+ // The promise of this createSession will be either resolved
+ // or rejected after provisioning.
+ mDrm.closeSession(sessionId.array());
+ }
+ savePendingCreateSessionData(createSessionToken, promiseId,
+ initData, initDataType);
+ startProvisioning(promiseId);
+ }
+ }
+
+ @Override
+ public void updateSession(int promiseId,
+ String sessionId,
+ byte[] response) {
+ if (DEBUG) Log.d(LOGTAG, "updateSession(), sessionId = " + sessionId);
+ if (mDrm == null) {
+ onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
+ return;
+ }
+
+ ByteBuffer session = ByteBuffer.wrap(sessionId.getBytes());
+ if (!sessionExists(session)) {
+ onRejectPromise(promiseId, "Invalid session during updateSession.");
+ return;
+ }
+
+ try {
+ final byte [] keySetId = mDrm.provideKeyResponse(session.array(), response);
+ if (DEBUG) {
+ HashMap<String, String> infoMap = mDrm.queryKeyStatus(session.array());
+ for (String strKey : infoMap.keySet()) {
+ String strValue = infoMap.get(strKey);
+ Log.d(LOGTAG, "InfoMap : key(" + strKey + ")/value(" + strValue + ")");
+ }
+ }
+ SessionKeyInfo[] keyInfos = new SessionKeyInfo[1];
+ keyInfos[0] = new SessionKeyInfo(DUMMY_KEY_ID,
+ MediaDrm.KeyStatus.STATUS_USABLE);
+ onSessionBatchedKeyChanged(session.array(), keyInfos);
+ if (DEBUG) Log.d(LOGTAG, "Key successfully added for session " + sessionId);
+ onSessionUpdated(promiseId, session.array());
+ return;
+ } catch (android.media.NotProvisionedException e) {
+ if (DEBUG) Log.d(LOGTAG, "Failed to provide key response:" + e.getMessage());
+ onSessionError(session.array(), "Got NotProvisionedException.");
+ onRejectPromise(promiseId, "Not provisioned during updateSession.");
+ } catch (android.media.DeniedByServerException e) {
+ if (DEBUG) Log.d(LOGTAG, "Failed to provide key response:" + e.getMessage());
+ onSessionError(session.array(), "Got DeniedByServerException.");
+ onRejectPromise(promiseId, "Denied by server during updateSession.");
+ } catch (java.lang.IllegalStateException e) {
+ if (DEBUG) Log.d(LOGTAG, "Exception when calling provideKeyResponse():" + e.getMessage());
+ onSessionError(session.array(), "Got IllegalStateException.");
+ onRejectPromise(promiseId, "Rejected during updateSession.");
+ }
+ release();
+ return;
+ }
+
+ @Override
+ public void closeSession(int promiseId, String sessionId) {
+ if (DEBUG) Log.d(LOGTAG, "closeSession()");
+ if (mDrm == null) {
+ onRejectPromise(promiseId, "MediaDrm instance doesn't exist !!");
+ return;
+ }
+
+ ByteBuffer session = ByteBuffer.wrap(sessionId.getBytes());
+ mSessionIds.remove(session);
+ mDrm.closeSession(session.array());
+ onSessionClosed(promiseId, session.array());
+ }
+
+ @Override
+ public void release() {
+ if (DEBUG) Log.d(LOGTAG, "release()");
+ if (mProvisioningPromiseId > 0) {
+ onRejectPromise(mProvisioningPromiseId, "Releasing ... reject provisioning session.");
+ mProvisioningPromiseId = 0;
+ }
+ while (!mPendingCreateSessionDataQueue.isEmpty()) {
+ PendingCreateSessionData pendingData = mPendingCreateSessionDataQueue.poll();
+ onRejectPromise(pendingData.mPromiseId, "Releasing ... reject all pending sessions.");
+ }
+ mPendingCreateSessionDataQueue = null;
+
+ if (mDrm != null) {
+ for (ByteBuffer session : mSessionIds) {
+ mDrm.closeSession(session.array());
+ }
+ mDrm.release();
+ mDrm = null;
+ }
+ mSessionIds.clear();
+ mSessionIds = null;
+ mSessionMIMETypes.clear();
+ mSessionMIMETypes = null;
+
+ mCryptoSessionId = null;
+ if (mCrypto != null) {
+ mCrypto.release();
+ mCrypto = null;
+ }
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread = null;
+ }
+ mHandler = null;
+ }
+
+ @Override
+ public MediaCrypto getMediaCrypto() {
+ if (DEBUG) Log.d(LOGTAG, "getMediaCrypto()");
+ return mCrypto;
+ }
+
+ protected void onSessionCreated(int createSessionToken,
+ int promiseId,
+ byte[] sessionId,
+ byte[] request) {
+ assertTrue(mCallbacks != null);
+ mCallbacks.onSessionCreated(createSessionToken, promiseId, sessionId, request);
+ }
+
+ protected void onSessionUpdated(int promiseId, byte[] sessionId) {
+ assertTrue(mCallbacks != null);
+ mCallbacks.onSessionUpdated(promiseId, sessionId);
+ }
+
+ protected void onSessionClosed(int promiseId, byte[] sessionId) {
+ assertTrue(mCallbacks != null);
+ mCallbacks.onSessionClosed(promiseId, sessionId);
+ }
+
+ protected void onSessionMessage(byte[] sessionId,
+ int sessionMessageType,
+ byte[] request) {
+ assertTrue(mCallbacks != null);
+ mCallbacks.onSessionMessage(sessionId, sessionMessageType, request);
+ }
+
+ protected void onSessionError(byte[] sessionId, String message) {
+ assertTrue(mCallbacks != null);
+ mCallbacks.onSessionError(sessionId, message);
+ }
+
+ protected void onSessionBatchedKeyChanged(byte[] sessionId,
+ SessionKeyInfo[] keyInfos) {
+ assertTrue(mCallbacks != null);
+ mCallbacks.onSessionBatchedKeyChanged(sessionId, keyInfos);
+ }
+
+ protected void onRejectPromise(int promiseId, String message) {
+ assertTrue(mCallbacks != null);
+ mCallbacks.onRejectPromise(promiseId, message);
+ }
+
+ private MediaDrm.KeyRequest getKeyRequest(ByteBuffer aSession,
+ byte[] data,
+ String mimeType)
+ throws android.media.NotProvisionedException {
+ if (mProvisioningPromiseId > 0) {
+ // Now provisioning.
+ return null;
+ }
+
+ try {
+ HashMap<String, String> optionalParameters = new HashMap<String, String>();
+ return mDrm.getKeyRequest(aSession.array(),
+ data,
+ mimeType,
+ MediaDrm.KEY_TYPE_STREAMING,
+ optionalParameters);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Got excpetion during MediaDrm.getKeyRequest", e);
+ }
+ return null;
+ }
+
+ private class MediaDrmListener implements MediaDrm.OnEventListener {
+ @Override
+ public void onEvent(MediaDrm mediaDrm, byte[] sessionArray, int event,
+ int extra, byte[] data) {
+ if (DEBUG) Log.d(LOGTAG, "MediaDrmListener.onEvent()");
+ if (sessionArray == null) {
+ if (DEBUG) Log.d(LOGTAG, "MediaDrmListener: Null session.");
+ return;
+ }
+ ByteBuffer session = ByteBuffer.wrap(sessionArray);
+ if (!sessionExists(session)) {
+ if (DEBUG) Log.d(LOGTAG, "MediaDrmListener: Invalid session.");
+ return;
+ }
+ // On L, these events are treated as exceptions and handled correspondingly.
+ // Leaving this code block for logging message.
+ String sessionId = new String(session.array());
+ switch (event) {
+ case MediaDrm.EVENT_PROVISION_REQUIRED:
+ if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_PROVISION_REQUIRED");
+ break;
+ case MediaDrm.EVENT_KEY_REQUIRED:
+ if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_KEY_REQUIRED");
+ // No need to handle here if we're not in privacy mode.
+ break;
+ case MediaDrm.EVENT_KEY_EXPIRED:
+ if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_KEY_EXPIRED, sessionId=" + sessionId);
+ break;
+ case MediaDrm.EVENT_VENDOR_DEFINED:
+ if (DEBUG) Log.d(LOGTAG, "MediaDrm.EVENT_VENDOR_DEFINED, sessionId=" + sessionId);
+ break;
+ default:
+ if (DEBUG) Log.d(LOGTAG, "Invalid DRM event " + event);
+ return;
+ }
+ }
+ }
+
+ private ByteBuffer openSession() throws android.media.NotProvisionedException {
+ try {
+ byte[] sessionId = mDrm.openSession();
+ // ByteBuffer.wrap() is backed by the byte[]. Make a clone here in
+ // case the underlying byte[] is modified.
+ return ByteBuffer.wrap(sessionId.clone());
+ } catch (android.media.NotProvisionedException e) {
+ // Throw NotProvisionedException so that we can startProvisioning().
+ throw e;
+ } catch (java.lang.RuntimeException e) {
+ if (DEBUG) Log.d(LOGTAG, "Cannot open a new session:" + e.getMessage());
+ release();
+ return null;
+ } catch (android.media.MediaDrmException e) {
+ // Other MediaDrmExceptions (e.g. ResourceBusyException) are not
+ // recoverable.
+ release();
+ return null;
+ }
+ }
+
+ private boolean sessionExists(ByteBuffer session) {
+ if (mCryptoSessionId == null) {
+ if (DEBUG) Log.d(LOGTAG, "Session doesn't exist because media crypto session is not created.");
+ return false;
+ }
+ if (session == null) {
+ if (DEBUG) Log.d(LOGTAG, "Session is null, not in map !");
+ return false;
+ }
+ return !session.equals(mCryptoSessionId) && mSessionIds.contains(session);
+ }
+
+ private class PostRequestTask extends AsyncTask<Void, Void, Void> {
+ private static final String LOGTAG = "PostRequestTask";
+
+ private int mPromiseId;
+ private String mURL;
+ private byte[] mDrmRequest;
+ private byte[] mResponseBody;
+
+ PostRequestTask(int promiseId, String url, byte[] drmRequest) {
+ this.mPromiseId = promiseId;
+ this.mURL = url;
+ this.mDrmRequest = drmRequest;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ URL finalURL = new URL(mURL + "&signedRequest=" + URLEncoder.encode(new String(mDrmRequest), "UTF-8"));
+ HttpURLConnection urlConnection = (HttpURLConnection) finalURL.openConnection();
+ urlConnection.setRequestMethod("POST");
+ if (DEBUG) Log.d(LOGTAG, "Provisioning, posting url =" + finalURL.toString());
+
+ // Add data
+ urlConnection.setRequestProperty("Accept", "*/*");
+ urlConnection.setRequestProperty("User-Agent", getCDMUserAgent());
+ urlConnection.setRequestProperty("Content-Type", "application/json");
+
+ // Execute HTTP Post Request
+ urlConnection.connect();
+
+ int responseCode = urlConnection.getResponseCode();
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ BufferedReader in =
+ new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
+ String inputLine;
+ StringBuffer response = new StringBuffer();
+
+ while ((inputLine = in.readLine()) != null) {
+ response.append(inputLine);
+ }
+ in.close();
+ mResponseBody = String.valueOf(response).getBytes();
+ if (DEBUG) Log.d(LOGTAG, "Provisioning, response received.");
+ if (mResponseBody != null) Log.d(LOGTAG, "response length=" + mResponseBody.length);
+ } else {
+ Log.d(LOGTAG, "Provisioning, server returned HTTP error code :" + responseCode);
+ }
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Got exception during posting provisioning request ...", e);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void v) {
+ onProvisionResponse(mPromiseId, mResponseBody);
+ }
+ }
+
+ private boolean provideProvisionResponse(byte[] response) {
+ if (response == null || response.length == 0) {
+ if (DEBUG) Log.d(LOGTAG, "Invalid provision response.");
+ return false;
+ }
+
+ try {
+ mDrm.provideProvisionResponse(response);
+ return true;
+ } catch (android.media.DeniedByServerException e) {
+ if (DEBUG) Log.d(LOGTAG, "Failed to provide provision response:" + e.getMessage());
+ } catch (java.lang.IllegalStateException e) {
+ if (DEBUG) Log.d(LOGTAG, "Failed to provide provision response:" + e.getMessage());
+ }
+ return false;
+ }
+
+ private void savePendingCreateSessionData(int token,
+ int promiseId,
+ byte[] initData,
+ String mime) {
+ if (DEBUG) Log.d(LOGTAG, "savePendingCreateSessionData, promiseId : " + promiseId);
+ mPendingCreateSessionDataQueue.offer(new PendingCreateSessionData(token, promiseId, initData, mime));
+ }
+
+ private void processPendingCreateSessionData() {
+ if (DEBUG) Log.d(LOGTAG, "processPendingCreateSessionData ... ");
+
+ assertTrue(mProvisioningPromiseId == 0);
+ try {
+ while (!mPendingCreateSessionDataQueue.isEmpty()) {
+ PendingCreateSessionData pendingData = mPendingCreateSessionDataQueue.poll();
+ if (DEBUG) Log.d(LOGTAG, "processPendingCreateSessionData, promiseId : " + pendingData.mPromiseId);
+
+ createSession(pendingData.mToken,
+ pendingData.mPromiseId,
+ pendingData.mMimeType,
+ pendingData.mInitData);
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Got excpetion during processPendingCreateSessionData ...", e);
+ }
+ }
+
+ private void resumePendingOperations() {
+ if (mHandlerThread == null) {
+ mHandlerThread = new HandlerThread("PendingSessionOpsThread");
+ mHandlerThread.start();
+ }
+ if (mHandler == null) {
+ mHandler = new Handler(mHandlerThread.getLooper());
+ }
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ processPendingCreateSessionData();
+ }
+ });
+ }
+
+ // Only triggered when failed on {openSession, getKeyRequest}
+ private void startProvisioning(int promiseId) {
+ if (DEBUG) Log.d(LOGTAG, "startProvisioning()");
+ if (mProvisioningPromiseId > 0) {
+ // Already in provisioning.
+ return;
+ }
+ try {
+ mProvisioningPromiseId = promiseId;
+ MediaDrm.ProvisionRequest request = mDrm.getProvisionRequest();
+ PostRequestTask postTask =
+ new PostRequestTask(promiseId, request.getDefaultUrl(), request.getData());
+ postTask.execute();
+ } catch (Exception e) {
+ onRejectPromise(promiseId, "Exception happened in startProvisioning !");
+ mProvisioningPromiseId = 0;
+ }
+ }
+
+ private void onProvisionResponse(int promiseId, byte[] response) {
+ if (DEBUG) Log.d(LOGTAG, "onProvisionResponse()");
+
+ mProvisioningPromiseId = 0;
+ boolean success = provideProvisionResponse(response);
+ if (success) {
+ // Promise will either be resovled / rejected in createSession during
+ // resuming operations.
+ resumePendingOperations();
+ } else {
+ onRejectPromise(promiseId, "Failed to provide provision response.");
+ }
+ }
+
+ private boolean ensureMediaCryptoCreated() throws android.media.NotProvisionedException {
+ if (mCrypto != null) {
+ return true;
+ }
+ try {
+ mCryptoSessionId = openSession();
+ if (mCryptoSessionId == null) {
+ if (DEBUG) Log.d(LOGTAG, "Cannot open session for MediaCrypto");
+ return false;
+ }
+
+ if (MediaCrypto.isCryptoSchemeSupported(mSchemeUUID)) {
+ final byte [] cryptoSessionId = mCryptoSessionId.array();
+ mCrypto = new MediaCrypto(mSchemeUUID, cryptoSessionId);
+ String strCryptoSessionId = new String(cryptoSessionId);
+ mSessionIds.add(mCryptoSessionId);
+ if (DEBUG) Log.d(LOGTAG, "MediaCrypto successfully created! - SId " + INVALID_SESSION_ID + ", " + strCryptoSessionId);
+ return true;
+ } else {
+ if (DEBUG) Log.d(LOGTAG, "Cannot create MediaCrypto for unsupported scheme.");
+ return false;
+ }
+ } catch (android.media.MediaCryptoException e) {
+ if (DEBUG) Log.d(LOGTAG, "Cannot create MediaCrypto:" + e.getMessage());
+ release();
+ return false;
+ } catch (android.media.NotProvisionedException e) {
+ if (DEBUG) Log.d(LOGTAG, "ensureMediaCryptoCreated::Device not provisioned:" + e.getMessage());
+ throw e;
+ }
+ }
+
+ private UUID convertKeySystemToSchemeUUID(String keySystem) {
+ if (WIDEVINE_KEY_SYSTEM.equals(keySystem)) {
+ return WIDEVINE_SCHEME_UUID;
+ }
+ if (DEBUG) Log.d(LOGTAG, "Cannot convert unsupported key system : " + keySystem);
+ return null;
+ }
+
+ private String getCDMUserAgent() {
+ // This user agent is found and hard-coded in Android(L) source code and
+ // Chromium project. Not sure if it's gonna change in the future.
+ String ua = "Widevine CDM v1.0";
+ return ua;
+ }
+}