blob: 2c1d9c5edc4640512e5854b62702a7cfb238a060 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.media.drmframework.cts;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaDrm;
import android.media.MediaDrm.KeyStatus;
import android.media.MediaDrm.MediaDrmStateException;
import android.media.MediaDrmException;
import android.media.MediaFormat;
import android.media.NotProvisionedException;
import android.media.ResourceBusyException;
import android.media.UnsupportedSchemeException;
import android.media.cts.AudioManagerStub;
import android.media.cts.AudioManagerStubHelper;
import android.media.cts.ConnectionStatus;
import android.media.cts.IConnectionStatus;
import android.media.cts.InputSurface;
import android.media.cts.InputSurfaceInterface;
import android.media.cts.MediaCodecClearKeyPlayer;
import android.media.cts.MediaCodecPlayerTestBase;
import android.media.cts.MediaCodecWrapper;
import android.media.cts.MediaTimeProvider;
import android.media.cts.MediaStubActivity;
import android.media.cts.NdkInputSurface;
import android.media.cts.NdkMediaCodec;
import android.media.cts.TestUtils.Monitor;
import android.media.cts.Utils;
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
import android.platform.test.annotations.Presubmit;
import android.util.Base64;
import android.util.Log;
import android.view.Surface;
import com.android.compatibility.common.util.ApiLevelUtil;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.Vector;
import androidx.annotation.NonNull;
import androidx.test.filters.SdkSuppress;
/**
* Tests of MediaPlayer streaming capabilities.
*/
public class MediaDrmClearkeyTest extends MediaCodecPlayerTestBase<MediaStubActivity> {
private static final String TAG = MediaDrmClearkeyTest.class.getSimpleName();
// Add additional keys here if the content has more keys.
private static final byte[] CLEAR_KEY_CENC = {
(byte)0x3f, (byte)0x0a, (byte)0x33, (byte)0xf3, (byte)0x40, (byte)0x98, (byte)0xb9, (byte)0xe2,
(byte)0x2b, (byte)0xc0, (byte)0x78, (byte)0xe0, (byte)0xa1, (byte)0xb5, (byte)0xe8, (byte)0x54 };
private static final byte[] CLEAR_KEY_WEBM = "_CLEAR_KEY_WEBM_".getBytes();
private static final int NUMBER_OF_SECURE_STOPS = 10;
private static final int VIDEO_WIDTH_CENC = 1280;
private static final int VIDEO_HEIGHT_CENC = 720;
private static final int VIDEO_WIDTH_WEBM = 352;
private static final int VIDEO_HEIGHT_WEBM = 288;
private static final int VIDEO_WIDTH_MPEG2TS = 320;
private static final int VIDEO_HEIGHT_MPEG2TS = 240;
private static final String MIME_VIDEO_AVC = MediaFormat.MIMETYPE_VIDEO_AVC;
private static final String MIME_VIDEO_VP8 = MediaFormat.MIMETYPE_VIDEO_VP8;
// Property Keys
private static final String ALGORITHMS_PROPERTY_KEY = MediaDrm.PROPERTY_ALGORITHMS;
private static final String DESCRIPTION_PROPERTY_KEY = MediaDrm.PROPERTY_DESCRIPTION;
private static final String DEVICEID_PROPERTY_KEY = "deviceId";
private static final String INVALID_PROPERTY_KEY = "invalid property key";
private static final String LISTENER_TEST_SUPPORT_PROPERTY_KEY = "listenerTestSupport";
private static final String VENDOR_PROPERTY_KEY = MediaDrm.PROPERTY_VENDOR;
private static final String VERSION_PROPERTY_KEY = MediaDrm.PROPERTY_VERSION;
// Error message
private static final String ERR_MSG_CRYPTO_SCHEME_NOT_SUPPORTED = "Crypto scheme is not supported";
private static final String CENC_AUDIO_PATH = "/clear/h264/llama/llama_aac_audio.mp4";
private static final String CENC_VIDEO_PATH = "/clearkey/llama_h264_main_720p_8000.mp4";
private static final Uri WEBM_URL = Uri.parse(
"android.resource://android.media.drmframework.cts/" + R.raw.video_320x240_webm_vp8_800kbps_30fps_vorbis_stereo_128kbps_44100hz_crypt);
private static final Uri MPEG2TS_SCRAMBLED_URL = Uri.parse(
"android.resource://android.media.drmframework.cts/" + R.raw.segment000001_scrambled);
private static final Uri MPEG2TS_CLEAR_URL = Uri.parse(
"android.resource://android.media.drmframework.cts/" + R.raw.segment000001);
private static final UUID COMMON_PSSH_SCHEME_UUID =
new UUID(0x1077efecc0b24d02L, 0xace33c1e52e2fb4bL);
private static final UUID CLEARKEY_SCHEME_UUID =
new UUID(0xe2719d58a985b3c9L, 0x781ab030af78d30eL);
private byte[] mDrmInitData;
private byte[] mKeySetId;
private byte[] mSessionId;
private Monitor mSessionMonitor = new Monitor();
private Looper mLooper;
private MediaDrm mDrm = null;
private final Object mLock = new Object();
private boolean mEventListenerCalled;
private boolean mExpirationUpdateReceived;
private boolean mLostStateReceived;
private static boolean sIsAtLeastS = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.S);
public MediaDrmClearkeyTest() {
super(MediaStubActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
if (false == deviceHasMediaDrm()) {
tearDown();
}
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
}
private boolean deviceHasMediaDrm() {
// ClearKey is introduced after KitKat.
if (ApiLevelUtil.isAtMost(android.os.Build.VERSION_CODES.KITKAT)) {
return false;
}
return true;
}
/**
* Extracts key ids from the pssh blob returned by getKeyRequest() and
* places it in keyIds.
* keyRequestBlob format (section 5.1.3.1):
* https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html#clear-key
*
* @return size of keyIds vector that contains the key ids, 0 for error
*/
private static int getKeyIds(byte[] keyRequestBlob, Vector<String> keyIds) {
if (0 == keyRequestBlob.length || keyIds == null)
return 0;
String jsonLicenseRequest = new String(keyRequestBlob);
keyIds.clear();
try {
JSONObject license = new JSONObject(jsonLicenseRequest);
final JSONArray ids = license.getJSONArray("kids");
for (int i = 0; i < ids.length(); ++i) {
keyIds.add(ids.getString(i));
}
} catch (JSONException e) {
Log.e(TAG, "Invalid JSON license = " + jsonLicenseRequest);
return 0;
}
return keyIds.size();
}
/**
* Creates the JSON Web Key string.
*
* @return JSON Web Key string.
*/
private static String createJsonWebKeySet(
Vector<String> keyIds, Vector<String> keys, int keyType) {
String jwkSet = "{\"keys\":[";
for (int i = 0; i < keyIds.size(); ++i) {
String id = new String(keyIds.get(i).getBytes(Charset.forName("UTF-8")));
String key = new String(keys.get(i).getBytes(Charset.forName("UTF-8")));
jwkSet += "{\"kty\":\"oct\",\"kid\":\"" + id +
"\",\"k\":\"" + key + "\"}";
}
jwkSet += "], \"type\":";
if (keyType == MediaDrm.KEY_TYPE_OFFLINE || keyType == MediaDrm.KEY_TYPE_RELEASE) {
jwkSet += "\"persistent-license\" }";
} else {
jwkSet += "\"temporary\" }";
}
return jwkSet;
}
/**
* Retrieves clear key ids from getKeyRequest(), create JSON Web Key
* set and send it to the CDM via provideKeyResponse().
*
* @return key set ID
*/
public static byte[] retrieveKeys(MediaDrm drm, String initDataType,
byte[] sessionId, byte[] drmInitData, int keyType, byte[][] clearKeyIds) {
MediaDrm.KeyRequest drmRequest = null;
try {
drmRequest = drm.getKeyRequest(sessionId, drmInitData, initDataType,
keyType, null);
} catch (Exception e) {
e.printStackTrace();
Log.i(TAG, "Failed to get key request: " + e.toString());
}
if (drmRequest == null) {
Log.e(TAG, "Failed getKeyRequest");
return null;
}
Vector<String> keyIds = new Vector<String>();
if (0 == getKeyIds(drmRequest.getData(), keyIds)) {
Log.e(TAG, "No key ids found in initData");
return null;
}
if (clearKeyIds.length != keyIds.size()) {
Log.e(TAG, "Mismatch number of key ids and keys: ids=" +
keyIds.size() + ", keys=" + clearKeyIds.length);
return null;
}
// Base64 encodes clearkeys. Keys are known to the application.
Vector<String> keys = new Vector<String>();
for (int i = 0; i < clearKeyIds.length; ++i) {
String clearKey = Base64.encodeToString(clearKeyIds[i],
Base64.NO_PADDING | Base64.NO_WRAP);
keys.add(clearKey);
}
String jwkSet = createJsonWebKeySet(keyIds, keys, keyType);
byte[] jsonResponse = jwkSet.getBytes(Charset.forName("UTF-8"));
try {
try {
return drm.provideKeyResponse(sessionId, jsonResponse);
} catch (IllegalStateException e) {
Log.e(TAG, "Failed to provide key response: " + e.toString());
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "Failed to provide key response: " + e.toString());
}
return null;
}
/**
* Retrieves clear key ids from getKeyRequest(), create JSON Web Key
* set and send it to the CDM via provideKeyResponse().
*/
private void getKeys(MediaDrm drm, String initDataType,
byte[] sessionId, byte[] drmInitData, int keyType, byte[][] clearKeyIds) {
mKeySetId = retrieveKeys(drm, initDataType, sessionId, drmInitData, keyType, clearKeyIds);
}
private @NonNull MediaDrm startDrm(final byte[][] clearKeyIds, final String initDataType,
final UUID drmSchemeUuid, int keyType) {
if (!MediaDrm.isCryptoSchemeSupported(drmSchemeUuid)) {
throw new Error(ERR_MSG_CRYPTO_SCHEME_NOT_SUPPORTED);
}
new Thread() {
@Override
public void run() {
if (mDrm != null) {
Log.e(TAG, "Failed to startDrm: already started");
return;
}
// Set up a looper to handle events
Looper.prepare();
// Save the looper so that we can terminate this thread
// after we are done with it.
mLooper = Looper.myLooper();
try {
mDrm = new MediaDrm(drmSchemeUuid);
} catch (MediaDrmException e) {
Log.e(TAG, "Failed to create MediaDrm: " + e.getMessage());
return;
}
synchronized(mLock) {
mDrm.setOnEventListener(new MediaDrm.OnEventListener() {
@Override
public void onEvent(MediaDrm md, byte[] sid, int event,
int extra, byte[] data) {
if (md != mDrm) {
Log.e(TAG, "onEvent callback: drm object mismatch");
return;
} else if (!Arrays.equals(mSessionId, sid)) {
Log.e(TAG, "onEvent callback: sessionId mismatch: |" +
Arrays.toString(mSessionId) + "| vs |" + Arrays.toString(sid) + "|");
return;
}
mEventListenerCalled = true;
if (event == MediaDrm.EVENT_PROVISION_REQUIRED) {
Log.i(TAG, "MediaDrm event: Provision required");
} else if (event == MediaDrm.EVENT_KEY_REQUIRED) {
Log.i(TAG, "MediaDrm event: Key required");
getKeys(mDrm, initDataType, mSessionId, mDrmInitData,
keyType, clearKeyIds);
} else if (event == MediaDrm.EVENT_KEY_EXPIRED) {
Log.i(TAG, "MediaDrm event: Key expired");
getKeys(mDrm, initDataType, mSessionId, mDrmInitData,
keyType, clearKeyIds);
} else if (event == MediaDrm.EVENT_VENDOR_DEFINED) {
Log.i(TAG, "MediaDrm event: Vendor defined");
} else if (event == MediaDrm.EVENT_SESSION_RECLAIMED) {
Log.i(TAG, "MediaDrm event: Session reclaimed");
} else {
Log.e(TAG, "MediaDrm event not supported: " + event);
}
}
});
mDrm.setOnExpirationUpdateListener(new MediaDrm.OnExpirationUpdateListener() {
@Override
public void onExpirationUpdate(MediaDrm md, byte[] sid, long expirationTime) {
if (md != mDrm) {
Log.e(TAG, "onExpirationUpdate callback: drm object mismatch");
} else if (!Arrays.equals(mSessionId, sid)) {
Log.e(TAG, "onExpirationUpdate callback: sessionId mismatch: |" +
Arrays.toString(mSessionId) + "| vs |" + Arrays.toString(sid) + "|");
} else {
mExpirationUpdateReceived = true;
}
}
}, null);
mDrm.setOnSessionLostStateListener(new MediaDrm.OnSessionLostStateListener() {
@Override
public void onSessionLostState(MediaDrm md, byte[] sid) {
if (md != mDrm) {
Log.e(TAG, "onSessionLostState callback: drm object mismatch");
} else if (!Arrays.equals(mSessionId, sid)) {
Log.e(TAG, "onSessionLostState callback: sessionId mismatch: |" +
Arrays.toString(mSessionId) + "| vs |" + Arrays.toString(sid) + "|");
} else {
mLostStateReceived = true;
}
}
}, null);
mDrm.setOnKeyStatusChangeListener(new MediaDrm.OnKeyStatusChangeListener() {
@Override
public void onKeyStatusChange(MediaDrm md, byte[] sessionId,
List<KeyStatus> keyInformation, boolean hasNewUsableKey) {
Log.d(TAG, "onKeyStatusChange");
assertTrue(md == mDrm);
assertTrue(Arrays.equals(sessionId, mSessionId));
mSessionMonitor.signal();
assertTrue(hasNewUsableKey);
assertEquals(3, keyInformation.size());
KeyStatus keyStatus = keyInformation.get(0);
assertTrue(Arrays.equals(keyStatus.getKeyId(), new byte[] {0xa, 0xb, 0xc}));
assertTrue(keyStatus.getStatusCode() == MediaDrm.KeyStatus.STATUS_USABLE);
keyStatus = keyInformation.get(1);
assertTrue(Arrays.equals(keyStatus.getKeyId(), new byte[] {0xd, 0xe, 0xf}));
assertTrue(keyStatus.getStatusCode() == MediaDrm.KeyStatus.STATUS_EXPIRED);
keyStatus = keyInformation.get(2);
assertTrue(Arrays.equals(keyStatus.getKeyId(), new byte[] {0x0, 0x1, 0x2}));
assertTrue(keyStatus.getStatusCode() == MediaDrm.KeyStatus.STATUS_USABLE_IN_FUTURE);
}
}, null);
mLock.notify();
}
Looper.loop(); // Blocks forever until Looper.quit() is called.
}
}.start();
// wait for mDrm to be created
synchronized(mLock) {
try {
mLock.wait(1000);
} catch (Exception e) {
}
}
return mDrm;
}
private void stopDrm(MediaDrm drm) {
if (drm != mDrm) {
Log.e(TAG, "invalid drm specified in stopDrm");
}
mLooper.quit();
mDrm.close();
mDrm = null;
}
private @NonNull byte[] openSession(MediaDrm drm) {
byte[] mSessionId = null;
boolean mRetryOpen;
do {
try {
mRetryOpen = false;
mSessionId = drm.openSession();
} catch (Exception e) {
mRetryOpen = true;
}
} while (mRetryOpen);
return mSessionId;
}
private void closeSession(MediaDrm drm, byte[] sessionId) {
drm.closeSession(sessionId);
}
/**
* Tests clear key system playback.
*/
private void testClearKeyPlayback(
UUID drmSchemeUuid,
String videoMime, String[] videoFeatures,
String initDataType, byte[][] clearKeyIds,
Uri audioUrl, boolean audioEncrypted,
Uri videoUrl, boolean videoEncrypted,
int videoWidth, int videoHeight, boolean scrambled, int keyType) throws Exception {
if (isWatchDevice()) {
return;
}
MediaDrm drm = null;
mSessionId = null;
final boolean hasDrm = !scrambled && drmSchemeUuid != null;
if (hasDrm) {
drm = startDrm(clearKeyIds, initDataType, drmSchemeUuid, keyType);
mSessionId = openSession(drm);
}
if (!preparePlayback(videoMime, videoFeatures, audioUrl, audioEncrypted, videoUrl,
videoEncrypted, videoWidth, videoHeight, scrambled, mSessionId, getSurfaces())) {
// Allow device to skip test to keep existing behavior.
// We should throw an exception for new tests.
return;
}
if (hasDrm) {
mDrmInitData = mMediaCodecPlayer.getDrmInitData();
getKeys(mDrm, initDataType, mSessionId, mDrmInitData, keyType, clearKeyIds);
}
if (hasDrm && keyType == MediaDrm.KEY_TYPE_OFFLINE) {
closeSession(drm, mSessionId);
mSessionMonitor.waitForSignal();
mSessionId = openSession(drm);
if (mKeySetId.length > 0) {
drm.restoreKeys(mSessionId, mKeySetId);
} else {
closeSession(drm, mSessionId);
stopDrm(drm);
throw new Error("Invalid keySetId size for offline license");
}
}
// starts video playback
playUntilEnd();
if (hasDrm) {
closeSession(drm, mSessionId);
stopDrm(drm);
}
}
/**
* Tests KEY_TYPE_RELEASE for offline license.
*/
@Presubmit
public void testReleaseOfflineLicense() throws Exception {
if (isWatchDevice()) {
return;
}
byte[][] clearKeyIds = new byte[][] { CLEAR_KEY_CENC };
mSessionId = null;
String initDataType = "cenc";
MediaDrm drm = startDrm(clearKeyIds, initDataType,
CLEARKEY_SCHEME_UUID, MediaDrm.KEY_TYPE_OFFLINE);
mSessionId = openSession(drm);
Uri videoUrl = Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH);
if (false == playbackPreCheck(MIME_VIDEO_AVC,
new String[] { CodecCapabilities.FEATURE_SecurePlayback }, videoUrl,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC)) {
// retry with unsecure codec
if (false == playbackPreCheck(MIME_VIDEO_AVC,
new String[0], videoUrl,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC)) {
Log.e(TAG, "Failed playback precheck");
return;
}
}
mMediaCodecPlayer = new MediaCodecClearKeyPlayer(
getSurfaces(),
mSessionId, false /*scrambled */,
mContext);
Uri audioUrl = Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH);
mMediaCodecPlayer.setAudioDataSource(audioUrl, null, false);
mMediaCodecPlayer.setVideoDataSource(videoUrl, null, true);
mMediaCodecPlayer.start();
mMediaCodecPlayer.prepare();
mDrmInitData = mMediaCodecPlayer.getDrmInitData();
// Create and store the offline license
getKeys(mDrm, initDataType, mSessionId, mDrmInitData, MediaDrm.KEY_TYPE_OFFLINE,
clearKeyIds);
// Verify the offline license is valid
closeSession(drm, mSessionId);
mSessionMonitor.waitForSignal();
mDrm.clearOnKeyStatusChangeListener();
mSessionId = openSession(drm);
drm.restoreKeys(mSessionId, mKeySetId);
closeSession(drm, mSessionId);
// Release the offline license
getKeys(mDrm, initDataType, mKeySetId, mDrmInitData, MediaDrm.KEY_TYPE_RELEASE,
clearKeyIds);
// Verify restoreKeys will throw an exception if the offline license
// has already been released
mSessionId = openSession(drm);
try {
drm.restoreKeys(mSessionId, mKeySetId);
} catch (MediaDrmStateException e) {
// Expected exception caught, all is good
return;
} finally {
closeSession(drm, mSessionId);
stopDrm(drm);
}
// Did not receive expected exception, throw an Error
throw new Error("Did not receive expected exception from restoreKeys");
}
private boolean queryKeyStatus(@NonNull final MediaDrm drm, @NonNull final byte[] sessionId) {
final HashMap<String, String> keyStatus = drm.queryKeyStatus(sessionId);
if (keyStatus.isEmpty()) {
Log.e(TAG, "queryKeyStatus: empty key status");
return false;
}
final Set<String> keySet = keyStatus.keySet();
final int numKeys = keySet.size();
final String[] keys = keySet.toArray(new String[numKeys]);
for (int i = 0; i < numKeys; ++i) {
final String key = keys[i];
Log.i(TAG, "queryKeyStatus: key=" + key + ", value=" + keyStatus.get(key));
}
return true;
}
@Presubmit
public void testQueryKeyStatus() throws Exception {
if (isWatchDevice()) {
// skip this test on watch because it calls
// addTrack that requires codec
return;
}
MediaDrm drm = startDrm(new byte[][] { CLEAR_KEY_CENC }, "cenc",
CLEARKEY_SCHEME_UUID, MediaDrm.KEY_TYPE_STREAMING);
mSessionId = openSession(drm);
// Test default key status, should not be defined
final HashMap<String, String> keyStatus = drm.queryKeyStatus(mSessionId);
if (!keyStatus.isEmpty()) {
closeSession(drm, mSessionId);
stopDrm(drm);
throw new Error("query default key status failed");
}
// Test valid key status
mMediaCodecPlayer = new MediaCodecClearKeyPlayer(
getSurfaces(),
mSessionId, false,
mContext);
mMediaCodecPlayer.setAudioDataSource(
Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), null, false);
mMediaCodecPlayer.setVideoDataSource(
Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), null, true);
mMediaCodecPlayer.start();
mMediaCodecPlayer.prepare();
mDrmInitData = mMediaCodecPlayer.getDrmInitData();
getKeys(drm, "cenc", mSessionId, mDrmInitData, MediaDrm.KEY_TYPE_STREAMING,
new byte[][] { CLEAR_KEY_CENC });
boolean success = true;
if (!queryKeyStatus(drm, mSessionId)) {
success = false;
}
mMediaCodecPlayer.reset();
closeSession(drm, mSessionId);
stopDrm(drm);
if (!success) {
throw new Error("query key status failed");
}
}
@Presubmit
public void testOfflineKeyManagement() throws Exception {
if (isWatchDevice()) {
// skip this test on watch because it calls
// addTrack that requires codec
return;
}
MediaDrm drm = startDrm(new byte[][] { CLEAR_KEY_CENC }, "cenc",
CLEARKEY_SCHEME_UUID, MediaDrm.KEY_TYPE_OFFLINE);
if (getClearkeyVersion(drm).matches("1.[01]")) {
Log.i(TAG, "Skipping testsOfflineKeyManagement: clearkey 1.2 required");
return;
}
mSessionId = openSession(drm);
// Test get offline keys
mMediaCodecPlayer = new MediaCodecClearKeyPlayer(
getSurfaces(),
mSessionId, false,
mContext);
mMediaCodecPlayer.setAudioDataSource(
Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), null, false);
mMediaCodecPlayer.setVideoDataSource(
Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), null, true);
mMediaCodecPlayer.start();
mMediaCodecPlayer.prepare();
try {
mDrmInitData = mMediaCodecPlayer.getDrmInitData();
List<byte[]> keySetIds = drm.getOfflineLicenseKeySetIds();
int preCount = keySetIds.size();
getKeys(drm, "cenc", mSessionId, mDrmInitData, MediaDrm.KEY_TYPE_OFFLINE,
new byte[][] { CLEAR_KEY_CENC });
if (drm.getOfflineLicenseState(mKeySetId) != MediaDrm.OFFLINE_LICENSE_STATE_USABLE) {
throw new Error("Offline license state is not usable");
}
keySetIds = drm.getOfflineLicenseKeySetIds();
if (keySetIds.size() != preCount + 1) {
throw new Error("KeySetIds size did not increment");
}
boolean found = false;
for (int i = 0; i < keySetIds.size(); i++) {
if (Arrays.equals(keySetIds.get(i), mKeySetId)) {
found = true;
break;
}
}
if (!found) {
throw new Error("New KeySetId is missing from KeySetIds");
}
drm.removeOfflineLicense(mKeySetId);
keySetIds = drm.getOfflineLicenseKeySetIds();
if (keySetIds.size() != preCount) {
throw new Error("KeySetIds size is incorrect");
}
found = false;
for (int i = 0; i < keySetIds.size(); i++) {
if (Arrays.equals(keySetIds.get(i), mKeySetId)) {
found = true;
break;
}
}
if (found) {
throw new Error("New KeySetId is still in from KeySetIds after removal");
}
// TODO: after RELEASE is implemented: add offline key, release it
// get offline key status, check state is inactive
} finally {
mMediaCodecPlayer.reset();
closeSession(drm, mSessionId);
stopDrm(drm);
}
}
// returns FEATURE_SecurePlayback if device supports secure codec,
// else returns an empty string for the codec feature
private String[] determineCodecFeatures(String mime,
int videoWidth, int videoHeight) {
String[] codecFeatures = { CodecCapabilities.FEATURE_SecurePlayback };
if (!isResolutionSupported(MIME_VIDEO_AVC, codecFeatures,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC)) {
// for device that does not support secure codec
codecFeatures = new String[0];
}
return codecFeatures;
}
public void testClearKeyPlaybackCenc() throws Exception {
String[] codecFeatures = determineCodecFeatures(MIME_VIDEO_AVC,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC);
testClearKeyPlayback(
COMMON_PSSH_SCHEME_UUID,
// using secure codec even though it is clear key DRM
MIME_VIDEO_AVC, codecFeatures,
"cenc", new byte[][] { CLEAR_KEY_CENC },
Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), false /* audioEncrypted */,
Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), true /* videoEncrypted */,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC, false /* scrambled */,
MediaDrm.KEY_TYPE_STREAMING);
}
@Presubmit
public void testClearKeyPlaybackCenc2() throws Exception {
String[] codecFeatures = determineCodecFeatures(MIME_VIDEO_AVC,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC);
testClearKeyPlayback(
CLEARKEY_SCHEME_UUID,
// using secure codec even though it is clear key DRM
MIME_VIDEO_AVC, codecFeatures,
"cenc", new byte[][] { CLEAR_KEY_CENC },
Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), false /* audioEncrypted */ ,
Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), true /* videoEncrypted */,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC, false /* scrambled */,
MediaDrm.KEY_TYPE_STREAMING);
}
@Presubmit
public void testClearKeyPlaybackOfflineCenc() throws Exception {
String[] codecFeatures = determineCodecFeatures(MIME_VIDEO_AVC,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC);
testClearKeyPlayback(
CLEARKEY_SCHEME_UUID,
// using secure codec even though it is clear key DRM
MIME_VIDEO_AVC, codecFeatures,
"cenc", new byte[][] { CLEAR_KEY_CENC },
Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), false /* audioEncrypted */ ,
Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), true /* videoEncrypted */,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC, false /* scrambled */,
MediaDrm.KEY_TYPE_OFFLINE);
}
public void testClearKeyPlaybackWebm() throws Exception {
testClearKeyPlayback(
CLEARKEY_SCHEME_UUID,
MIME_VIDEO_VP8, new String[0],
"webm", new byte[][] { CLEAR_KEY_WEBM },
WEBM_URL, true /* audioEncrypted */,
WEBM_URL, true /* videoEncrypted */,
VIDEO_WIDTH_WEBM, VIDEO_HEIGHT_WEBM, false /* scrambled */,
MediaDrm.KEY_TYPE_STREAMING);
}
public void testClearKeyPlaybackMpeg2ts() throws Exception {
testClearKeyPlayback(
CLEARKEY_SCHEME_UUID,
MIME_VIDEO_AVC, new String[0],
"mpeg2ts", null,
MPEG2TS_SCRAMBLED_URL, false /* audioEncrypted */,
MPEG2TS_SCRAMBLED_URL, false /* videoEncrypted */,
VIDEO_WIDTH_MPEG2TS, VIDEO_HEIGHT_MPEG2TS, true /* scrambled */,
MediaDrm.KEY_TYPE_STREAMING);
}
public void testPlaybackMpeg2ts() throws Exception {
testClearKeyPlayback(
CLEARKEY_SCHEME_UUID,
MIME_VIDEO_AVC, new String[0],
"mpeg2ts", null,
MPEG2TS_CLEAR_URL, false /* audioEncrypted */,
MPEG2TS_CLEAR_URL, false /* videoEncrypted */,
VIDEO_WIDTH_MPEG2TS, VIDEO_HEIGHT_MPEG2TS, false /* scrambled */,
MediaDrm.KEY_TYPE_STREAMING);
}
private String getStringProperty(final MediaDrm drm, final String key) {
String value = "";
try {
value = drm.getPropertyString(key);
} catch (IllegalArgumentException e) {
// Expected exception for invalid key
Log.d(TAG, "Expected result: " + e.getMessage());
} catch (Exception e) {
throw new Error(e.getMessage() + "-" + key);
}
return value;
}
private byte[] getByteArrayProperty(final MediaDrm drm, final String key) {
byte[] bytes = new byte[0];
try {
bytes = drm.getPropertyByteArray(key);
} catch (IllegalArgumentException e) {
// Expected exception for invalid key
Log.d(TAG, "Expected: " + e.getMessage() + " - " + key);
} catch (Exception e) {
throw new Error(e.getMessage() + "-" + key);
}
return bytes;
}
private void setStringProperty(final MediaDrm drm, final String key, final String value) {
try {
drm.setPropertyString(key, value);
} catch (IllegalArgumentException e) {
// Expected exception for invalid key
Log.d(TAG, "Expected: " + e.getMessage() + " - " + key);
} catch (Exception e) {
throw new Error(e.getMessage() + "-" + key);
}
}
private void setByteArrayProperty(final MediaDrm drm, final String key, final byte[] bytes) {
try {
drm.setPropertyByteArray(key, bytes);
} catch (IllegalArgumentException e) {
// Expected exception for invalid key
Log.d(TAG, "Expected: " + e.getMessage() + " - " + key);
} catch (Exception e) {
throw new Error(e.getMessage() + "-" + key);
}
}
@Presubmit
public void testGetProperties() throws Exception {
if (watchHasNoClearkeySupport()) {
return;
}
MediaDrm drm = startDrm(new byte[][] { CLEAR_KEY_CENC },
"cenc", CLEARKEY_SCHEME_UUID, MediaDrm.KEY_TYPE_STREAMING);
try {
// The following tests will not verify the value we are getting
// back since it could change in the future.
final String[] sKeys = {
DESCRIPTION_PROPERTY_KEY, LISTENER_TEST_SUPPORT_PROPERTY_KEY,
VENDOR_PROPERTY_KEY, VERSION_PROPERTY_KEY};
String value;
for (String key : sKeys) {
value = getStringProperty(drm, key);
Log.d(TAG, "getPropertyString returns: " + key + ", " + value);
if (value.isEmpty()) {
throw new Error("Failed to get property for: " + key);
}
}
if (cannotHandleGetPropertyByteArray(drm)) {
Log.i(TAG, "Skipping testGetProperties: byte array properties not implemented "
+ "on devices launched before P");
return;
}
byte[] bytes = getByteArrayProperty(drm, DEVICEID_PROPERTY_KEY);
if (0 == bytes.length) {
throw new Error("Failed to get property for: " + DEVICEID_PROPERTY_KEY);
}
// Test with an invalid property key.
value = getStringProperty(drm, INVALID_PROPERTY_KEY);
bytes = getByteArrayProperty(drm, INVALID_PROPERTY_KEY);
if (!value.isEmpty() || 0 != bytes.length) {
throw new Error("get property failed using an invalid property key");
}
} finally {
stopDrm(drm);
}
}
@Presubmit
public void testSetProperties() throws Exception {
if (watchHasNoClearkeySupport()) {
return;
}
MediaDrm drm = startDrm(new byte[][]{CLEAR_KEY_CENC},
"cenc", CLEARKEY_SCHEME_UUID, MediaDrm.KEY_TYPE_STREAMING);
try {
if (cannotHandleSetPropertyString(drm)) {
Log.i(TAG, "Skipping testSetProperties: set property string not implemented "
+ "on devices launched before P");
return;
}
// Test setting predefined string property
// - Save the value to be restored later
// - Set the property value
// - Check the value that was set
// - Restore previous value
String listenerTestSupport = getStringProperty(drm, LISTENER_TEST_SUPPORT_PROPERTY_KEY);
setStringProperty(drm, LISTENER_TEST_SUPPORT_PROPERTY_KEY, "testing");
String value = getStringProperty(drm, LISTENER_TEST_SUPPORT_PROPERTY_KEY);
if (!value.equals("testing")) {
throw new Error("Failed to set property: " + LISTENER_TEST_SUPPORT_PROPERTY_KEY);
}
setStringProperty(drm, LISTENER_TEST_SUPPORT_PROPERTY_KEY, listenerTestSupport);
// Test setting immutable properties
HashMap<String, String> defaultImmutableProperties = new HashMap<String, String>();
defaultImmutableProperties.put(ALGORITHMS_PROPERTY_KEY,
getStringProperty(drm, ALGORITHMS_PROPERTY_KEY));
defaultImmutableProperties.put(DESCRIPTION_PROPERTY_KEY,
getStringProperty(drm, DESCRIPTION_PROPERTY_KEY));
defaultImmutableProperties.put(VENDOR_PROPERTY_KEY,
getStringProperty(drm, VENDOR_PROPERTY_KEY));
defaultImmutableProperties.put(VERSION_PROPERTY_KEY,
getStringProperty(drm, VERSION_PROPERTY_KEY));
HashMap<String, String> immutableProperties = new HashMap<String, String>();
immutableProperties.put(ALGORITHMS_PROPERTY_KEY, "brute force");
immutableProperties.put(DESCRIPTION_PROPERTY_KEY, "testing only");
immutableProperties.put(VENDOR_PROPERTY_KEY, "my Google");
immutableProperties.put(VERSION_PROPERTY_KEY, "undefined");
for (String key : immutableProperties.keySet()) {
setStringProperty(drm, key, immutableProperties.get(key));
}
// Verify the immutable properties have not been set
for (String key : immutableProperties.keySet()) {
value = getStringProperty(drm, key);
if (!defaultImmutableProperties.get(key).equals(getStringProperty(drm, key))) {
throw new Error("Immutable property has changed, key=" + key);
}
}
// Test setPropertyByteArray for immutable property
final byte[] bytes = new byte[] {
0xf, 0xe, 0xd, 0xc, 0xb, 0xa, 0x9, 0x8,
0x7, 0x6, 0x5, 0x4, 0x3, 0x2, 0x1, 0x0};
final byte[] deviceId = getByteArrayProperty(drm, DEVICEID_PROPERTY_KEY);
setByteArrayProperty(drm, DEVICEID_PROPERTY_KEY, bytes);
// Verify deviceId has not changed
if (!Arrays.equals(deviceId, getByteArrayProperty(drm, DEVICEID_PROPERTY_KEY))) {
throw new Error("Failed to set byte array for key=" + DEVICEID_PROPERTY_KEY);
}
} finally {
stopDrm(drm);
}
}
private final static int CLEARKEY_MAX_SESSIONS = 10;
@Presubmit
public void testGetNumberOfSessions() {
if (watchHasNoClearkeySupport()) {
return;
}
MediaDrm drm = startDrm(new byte[][] { CLEAR_KEY_CENC },
"cenc", CLEARKEY_SCHEME_UUID, MediaDrm.KEY_TYPE_STREAMING);
try {
if (getClearkeyVersion(drm).equals("1.0")) {
Log.i(TAG, "Skipping testGetNumberOfSessions: not supported by clearkey 1.0");
return;
}
int maxSessionCount = drm.getMaxSessionCount();
if (maxSessionCount != CLEARKEY_MAX_SESSIONS) {
throw new Error("expected max session count to be " +
CLEARKEY_MAX_SESSIONS);
}
int initialOpenSessionCount = drm.getOpenSessionCount();
if (initialOpenSessionCount == maxSessionCount) {
throw new Error("all sessions open, can't do increment test");
}
mSessionId = openSession(drm);
try {
if (drm.getOpenSessionCount() != initialOpenSessionCount + 1) {
throw new Error("openSessionCount didn't increment");
}
} finally {
closeSession(drm, mSessionId);
}
} finally {
stopDrm(drm);
}
}
@Presubmit
public void testHdcpLevels() {
if (watchHasNoClearkeySupport()) {
return;
}
MediaDrm drm = null;
try {
drm = new MediaDrm(CLEARKEY_SCHEME_UUID);
if (getClearkeyVersion(drm).equals("1.0")) {
Log.i(TAG, "Skipping testHdcpLevels: not supported by clearkey 1.0");
return;
}
if (drm.getConnectedHdcpLevel() != MediaDrm.HDCP_NONE) {
throw new Error("expected connected hdcp level to be HDCP_NONE");
}
if (drm.getMaxHdcpLevel() != MediaDrm.HDCP_NO_DIGITAL_OUTPUT) {
throw new Error("expected max hdcp level to be HDCP_NO_DIGITAL_OUTPUT");
}
} catch(Exception e) {
throw new Error("Unexpected exception ", e);
} finally {
if (drm != null) {
drm.close();
}
}
}
@Presubmit
public void testSecurityLevels() {
if (watchHasNoClearkeySupport()) {
return;
}
MediaDrm drm = null;
byte[] sessionId = null;
try {
drm = new MediaDrm(CLEARKEY_SCHEME_UUID);
if (getClearkeyVersion(drm).equals("1.0")) {
Log.i(TAG, "Skipping testSecurityLevels: not supported by clearkey 1.0");
return;
}
sessionId = drm.openSession(MediaDrm.SECURITY_LEVEL_SW_SECURE_CRYPTO);
if (drm.getSecurityLevel(sessionId) != MediaDrm.SECURITY_LEVEL_SW_SECURE_CRYPTO) {
throw new Error("expected security level to be SECURITY_LEVEL_SW_SECURE_CRYPTO");
}
drm.closeSession(sessionId);
sessionId = null;
sessionId = drm.openSession();
if (drm.getSecurityLevel(sessionId) != MediaDrm.SECURITY_LEVEL_SW_SECURE_CRYPTO) {
throw new Error("expected security level to be SECURITY_LEVEL_SW_SECURE_CRYPTO");
}
drm.closeSession(sessionId);
sessionId = null;
try {
sessionId = drm.openSession(MediaDrm.SECURITY_LEVEL_SW_SECURE_DECODE);
} catch (IllegalArgumentException e) {
/* caught expected exception */
} catch (Exception e) {
throw new Exception ("did't get expected IllegalArgumentException" +
" while opening a session with disallowed security level");
} finally {
if (sessionId != null) {
drm.closeSession(sessionId);
sessionId = null;
}
}
} catch(Exception e) {
throw new Error("Unexpected exception ", e);
} finally {
if (sessionId != null) {
drm.closeSession(sessionId);
}
if (drm != null) {
drm.close();
}
}
}
@Presubmit
public void testSecureStop() {
if (watchHasNoClearkeySupport()) {
return;
}
MediaDrm drm = startDrm(new byte[][] {CLEAR_KEY_CENC}, "cenc",
CLEARKEY_SCHEME_UUID, MediaDrm.KEY_TYPE_STREAMING);
byte[] sessionId = null;
try {
if (getClearkeyVersion(drm).equals("1.0")) {
Log.i(TAG, "Skipping testSecureStop: not supported in ClearKey v1.0");
return;
}
drm.removeAllSecureStops();
Log.d(TAG, "Test getSecureStops from an empty list.");
List<byte[]> secureStops = drm.getSecureStops();
assertTrue(secureStops.isEmpty());
Log.d(TAG, "Test getSecureStopIds from an empty list.");
List<byte[]> secureStopIds = drm.getSecureStopIds();
assertTrue(secureStopIds.isEmpty());
mSessionId = openSession(drm);
mMediaCodecPlayer = new MediaCodecClearKeyPlayer(
getSurfaces(), mSessionId, false, mContext);
mMediaCodecPlayer.setAudioDataSource(
Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), null, false);
mMediaCodecPlayer.setVideoDataSource(
Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), null, true);
mMediaCodecPlayer.start();
mMediaCodecPlayer.prepare();
mDrmInitData = mMediaCodecPlayer.getDrmInitData();
for (int i = 0; i < NUMBER_OF_SECURE_STOPS; ++i) {
getKeys(drm, "cenc", mSessionId, mDrmInitData,
MediaDrm.KEY_TYPE_STREAMING, new byte[][] {CLEAR_KEY_CENC});
}
Log.d(TAG, "Test getSecureStops.");
secureStops = drm.getSecureStops();
assertEquals(NUMBER_OF_SECURE_STOPS, secureStops.size());
Log.d(TAG, "Test getSecureStopIds.");
secureStopIds = drm.getSecureStopIds();
assertEquals(NUMBER_OF_SECURE_STOPS, secureStopIds.size());
Log.d(TAG, "Test getSecureStop using secure stop Ids.");
for (int i = 0; i < secureStops.size(); ++i) {
byte[] secureStop = drm.getSecureStop(secureStopIds.get(i));
assertTrue(Arrays.equals(secureStops.get(i), secureStop));
}
Log.d(TAG, "Test removeSecureStop given a secure stop Id.");
drm.removeSecureStop(secureStopIds.get(NUMBER_OF_SECURE_STOPS - 1));
secureStops = drm.getSecureStops();
secureStopIds = drm.getSecureStopIds();
assertEquals(NUMBER_OF_SECURE_STOPS - 1, secureStops.size());
assertEquals(NUMBER_OF_SECURE_STOPS - 1, secureStopIds.size());
Log.d(TAG, "Test releaseSecureStops given a release message.");
// Simulate server response message by removing
// every other secure stops to make it interesting.
List<byte[]> releaseList = new ArrayList<byte[]>();
int releaseListSize = 0;
for (int i = 0; i < secureStops.size(); i += 2) {
byte[] secureStop = secureStops.get(i);
releaseList.add(secureStop);
releaseListSize += secureStop.length;
}
// ClearKey's release message format (this is a format shared between
// the server and the drm service).
// The clearkey implementation expects the message to contain
// a 4 byte count of the number of fixed length secure stops
// to follow.
String count = String.format("%04d", releaseList.size());
byte[] releaseMessage = new byte[count.length() + releaseListSize];
byte[] buffer = count.getBytes();
System.arraycopy(buffer, 0, releaseMessage, 0, count.length());
int destPosition = count.length();
for (int i = 0; i < releaseList.size(); ++i) {
byte[] secureStop = releaseList.get(i);
int secureStopSize = secureStop.length;
System.arraycopy(secureStop, 0, releaseMessage, destPosition, secureStopSize);
destPosition += secureStopSize;
}
drm.releaseSecureStops(releaseMessage);
secureStops = drm.getSecureStops();
secureStopIds = drm.getSecureStopIds();
// All odd numbered secure stops are removed in the test,
// leaving 2nd, 4th, 6th and the 8th element.
assertEquals((NUMBER_OF_SECURE_STOPS - 1) / 2, secureStops.size());
assertEquals((NUMBER_OF_SECURE_STOPS - 1 ) / 2, secureStopIds.size());
Log.d(TAG, "Test removeAllSecureStops.");
drm.removeAllSecureStops();
secureStops = drm.getSecureStops();
assertTrue(secureStops.isEmpty());
secureStopIds = drm.getSecureStopIds();
assertTrue(secureStopIds.isEmpty());
mMediaCodecPlayer.reset();
closeSession(drm, mSessionId);
} catch (Exception e) {
throw new Error("Unexpected exception", e);
} finally {
if (sessionId != null) {
drm.closeSession(sessionId);
}
stopDrm(drm);
}
}
/**
* Test that the framework handles a device returning
* ::android::hardware::drm@1.2::Status::ERROR_DRM_RESOURCE_CONTENTION.
* Expected behavior: throws MediaDrm.SessionException with
* errorCode ERROR_RESOURCE_CONTENTION
*/
@Presubmit
public void testResourceContentionError() {
if (watchHasNoClearkeySupport()) {
return;
}
MediaDrm drm = null;
boolean gotException = false;
try {
drm = new MediaDrm(CLEARKEY_SCHEME_UUID);
drm.setPropertyString("drmErrorTest", "resourceContention");
byte[] sessionId = drm.openSession();
try {
byte[] ignoredInitData = new byte[] { 1 };
drm.getKeyRequest(sessionId, ignoredInitData, "cenc", MediaDrm.KEY_TYPE_STREAMING, null);
} catch (MediaDrm.SessionException e) {
if (e.getErrorCode() != MediaDrm.SessionException.ERROR_RESOURCE_CONTENTION) {
throw new Error("Expected transient ERROR_RESOURCE_CONTENTION");
}
if(sIsAtLeastS && !e.isTransient()) {
throw new Error("Expected transient ERROR_RESOURCE_CONTENTION");
}
gotException = true;
}
} catch(Exception e) {
throw new Error("Unexpected exception ", e);
} finally {
if (drm != null) {
drm.close();
}
}
if (!gotException) {
throw new Error("Didn't receive expected MediaDrm.SessionException");
}
}
/**
* Test sendExpirationUpdate and onExpirationUpdateListener
*
* Expected behavior: the EXPIRATION_UPDATE event arrives
* at the onExpirationUpdateListener with the expiry time
*/
@Presubmit
public void testOnExpirationUpdateListener() {
if (watchHasNoClearkeySupport()) {
return;
}
MediaDrm drm = null;
mSessionId = null;
mExpirationUpdateReceived = false;
// provideKeyResponse calls sendExpirationUpdate method
// for testing purpose, we therefore start a license request
// which calls provideKeyResonpse
byte[][] clearKeyIds = new byte[][] { CLEAR_KEY_CENC };
int keyType = MediaDrm.KEY_TYPE_STREAMING;
String initDataType = new String("cenc");
drm = startDrm(clearKeyIds, initDataType, CLEARKEY_SCHEME_UUID, keyType);
mSessionId = openSession(drm);
try {
if (!preparePlayback(
MIME_VIDEO_AVC,
new String[0],
Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), false /* audioEncrypted */ ,
Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), true /* videoEncrypted */,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC, false /* scrambled */,
mSessionId, getSurfaces())) {
closeSession(drm, mSessionId);
stopDrm(drm);
return;
}
} catch (Exception e) {
throw new Error("Unexpected exception ", e);
}
mDrmInitData = mMediaCodecPlayer.getDrmInitData();
getKeys(drm, initDataType, mSessionId, mDrmInitData,
keyType, clearKeyIds);
// wait for the event to arrive
try {
closeSession(drm, mSessionId);
// wait up to 2 seconds for event
for (int i = 0; i < 20 && !mExpirationUpdateReceived; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
if (!mExpirationUpdateReceived) {
throw new Error("EXPIRATION_UPDATE event was not received by the listener");
}
} catch (MediaDrmStateException e) {
throw new Error("Unexpected exception from closing session: ", e);
} finally {
stopDrm(drm);
}
}
/**
* Test that the onExpirationUpdateListener
* listener is not called after
* clearOnExpirationUpdateListener is called.
*/
@Presubmit
public void testClearOnExpirationUpdateListener() {
if (watchHasNoClearkeySupport()) {
return;
}
MediaDrm drm = null;
mSessionId = null;
mExpirationUpdateReceived = false;
// provideKeyResponse calls sendExpirationUpdate method
// for testing purpose, we therefore start a license request
// which calls provideKeyResonpse
byte[][] clearKeyIds = new byte[][] { CLEAR_KEY_CENC };
int keyType = MediaDrm.KEY_TYPE_STREAMING;
String initDataType = new String("cenc");
drm = startDrm(clearKeyIds, initDataType, CLEARKEY_SCHEME_UUID, keyType);
mSessionId = openSession(drm);
try {
if (!preparePlayback(
MIME_VIDEO_AVC,
new String[0],
Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), false /* audioEncrypted */ ,
Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), true /* videoEncrypted */,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC, false /* scrambled */,
mSessionId, getSurfaces())) {
closeSession(drm, mSessionId);
stopDrm(drm);
return;
}
} catch (Exception e) {
throw new Error("Unexpected exception ", e);
}
// clear the expiration update listener
drm.clearOnExpirationUpdateListener();
mDrmInitData = mMediaCodecPlayer.getDrmInitData();
getKeys(drm, initDataType, mSessionId, mDrmInitData,
keyType, clearKeyIds);
// wait for the event, it should not arrive
// because the expiration update listener has been cleared
try {
closeSession(drm, mSessionId);
// wait up to 2 seconds for event
for (int i = 0; i < 20 && !mExpirationUpdateReceived; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
if (mExpirationUpdateReceived) {
throw new Error("onExpirationUpdateListener should not be called");
}
} catch (MediaDrmStateException e) {
throw new Error("Unexpected exception from closing session: ", e);
} finally {
stopDrm(drm);
}
}
/**
* Test that after onClearEventListener is called,
* MediaDrm's event listener is not called.
*
* Clearkey plugin's provideKeyResponse method sends a
* vendor defined event to the media drm event listener
* for testing purpose. Check that after onClearEventListener
* is called, the event listener is not called.
*/
@Presubmit
public void testClearOnEventListener() {
if (watchHasNoClearkeySupport()) {
return;
}
MediaDrm drm = null;
mSessionId = null;
mEventListenerCalled = false;
// provideKeyResponse in clearkey plugin sends a
// vendor defined event to test the event listener;
// we therefore start a license request which will
// call provideKeyResonpse
byte[][] clearKeyIds = new byte[][] { CLEAR_KEY_CENC };
int keyType = MediaDrm.KEY_TYPE_STREAMING;
String initDataType = new String("cenc");
drm = startDrm(clearKeyIds, initDataType, CLEARKEY_SCHEME_UUID, keyType);
mSessionId = openSession(drm);
try {
if (!preparePlayback(
MIME_VIDEO_AVC,
new String[0],
Uri.parse(Utils.getMediaPath() + CENC_AUDIO_PATH), false /* audioEncrypted */ ,
Uri.parse(Utils.getMediaPath() + CENC_VIDEO_PATH), true /* videoEncrypted */,
VIDEO_WIDTH_CENC, VIDEO_HEIGHT_CENC, false /* scrambled */,
mSessionId, getSurfaces())) {
closeSession(drm, mSessionId);
stopDrm(drm);
return;
}
} catch (Exception e) {
throw new Error("Unexpected exception ", e);
}
// test that the onEvent listener is called
mDrmInitData = mMediaCodecPlayer.getDrmInitData();
getKeys(drm, initDataType, mSessionId, mDrmInitData,
keyType, clearKeyIds);
// wait for the vendor defined event, it should not arrive
// because the event listener is cleared
try {
// wait up to 2 seconds for event
for (int i = 0; i < 20 && !mEventListenerCalled; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
if (!mEventListenerCalled) {
closeSession(drm, mSessionId);
stopDrm(drm);
throw new Error("onEventListener should be called");
}
} catch (MediaDrmStateException e) {
closeSession(drm, mSessionId);
stopDrm(drm);
throw new Error("Unexpected exception from closing session: ", e);
}
// clear the drm event listener
// and test that the onEvent listener is not called
mEventListenerCalled = false;
drm.clearOnEventListener();
getKeys(drm, initDataType, mSessionId, mDrmInitData,
keyType, clearKeyIds);
// wait for the vendor defined event, it should not arrive
// because the event listener is cleared
try {
closeSession(drm, mSessionId);
// wait up to 2 seconds for event
for (int i = 0; i < 20 && !mEventListenerCalled; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
if (mEventListenerCalled) {
throw new Error("onEventListener should not be called");
}
} catch (MediaDrmStateException e) {
throw new Error("Unexpected exception from closing session: ", e);
} finally {
stopDrm(drm);
}
}
/**
* Test that the framework handles a device returning invoking
* the ::android::hardware::drm@1.2::sendSessionLostState callback
* Expected behavior: OnSessionLostState is called with
* the sessionId
*/
@Presubmit
public void testSessionLostStateError() {
if (watchHasNoClearkeySupport()) {
return;
}
boolean gotException = false;
mLostStateReceived = false;
MediaDrm drm = startDrm(new byte[][] { CLEAR_KEY_CENC }, "cenc",
CLEARKEY_SCHEME_UUID, MediaDrm.KEY_TYPE_STREAMING);
mDrm.setPropertyString("drmErrorTest", "lostState");
mSessionId = openSession(drm);
// simulates session lost state here, detected by closeSession
try {
try {
closeSession(drm, mSessionId);
} catch (MediaDrmStateException e) {
gotException = true; // expected for lost state
}
// wait up to 2 seconds for event
for (int i = 0; i < 20 && !mLostStateReceived; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
if (!mLostStateReceived) {
throw new Error("Callback for OnSessionLostStateListener not received");
}
} catch(Exception e) {
throw new Error("Unexpected exception ", e);
} finally {
stopDrm(drm);
}
if (!gotException) {
throw new Error("Didn't receive expected MediaDrmStateException");
}
}
/**
* Test that the framework handles a device ignoring
* events for the onSessionLostStateListener after
* clearOnSessionLostStateListener is called.
*
* Expected behavior: OnSessionLostState is not called with
* the sessionId
*/
@Presubmit
public void testClearOnSessionLostStateListener() {
if (watchHasNoClearkeySupport()) {
return;
}
boolean gotException = false;
mLostStateReceived = false;
MediaDrm drm = startDrm(new byte[][] { CLEAR_KEY_CENC }, "cenc",
CLEARKEY_SCHEME_UUID, MediaDrm.KEY_TYPE_STREAMING);
mDrm.setPropertyString("drmErrorTest", "lostState");
mSessionId = openSession(drm);
// Simulates session lost state here, event is sent from closeSession.
// The session lost state should not arrive in the listener
// after clearOnSessionLostStateListener() is called.
try {
try {
mDrm.clearOnSessionLostStateListener();
Thread.sleep(2000);
closeSession(drm, mSessionId);
} catch (MediaDrmStateException e) {
gotException = true; // expected for lost state
}
// wait up to 2 seconds for event
for (int i = 0; i < 20 && !mLostStateReceived; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
if (mLostStateReceived) {
throw new Error("Should not receive callback for OnSessionLostStateListener");
}
} catch(Exception e) {
throw new Error("Unexpected exception ", e);
} finally {
stopDrm(drm);
}
if (!gotException) {
throw new Error("Didn't receive expected MediaDrmStateException");
}
}
@Presubmit
public void testIsCryptoSchemeSupportedWithSecurityLevel() {
if (watchHasNoClearkeySupport()) {
return;
}
if (MediaDrm.isCryptoSchemeSupported(CLEARKEY_SCHEME_UUID, "cenc",
MediaDrm.SECURITY_LEVEL_HW_SECURE_ALL)) {
throw new Error("Clearkey claims to support SECURITY_LEVEL_HW_SECURE_ALL");
}
if (!MediaDrm.isCryptoSchemeSupported(CLEARKEY_SCHEME_UUID, "cenc",
MediaDrm.SECURITY_LEVEL_SW_SECURE_CRYPTO)) {
throw new Error("Clearkey claims not to support SECURITY_LEVEL_SW_SECURE_CRYPTO");
}
}
@Presubmit
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public void testMediaDrmStateExceptionErrorCode()
throws ResourceBusyException, UnsupportedSchemeException, NotProvisionedException {
if (watchHasNoClearkeySupport()) {
return;
}
MediaDrm drm = null;
try {
drm = new MediaDrm(CLEARKEY_SCHEME_UUID);
byte[] sessionId = drm.openSession();
drm.closeSession(sessionId);
byte[] ignoredInitData = new byte[]{1};
drm.getKeyRequest(sessionId, ignoredInitData, "cenc",
MediaDrm.KEY_TYPE_STREAMING,
null);
} catch(MediaDrmStateException e) {
Log.i(TAG, "Verifying exception error code", e);
assertFalse("ERROR_SESSION_NOT_OPENED requires new session", e.isTransient());
assertEquals("Expected ERROR_SESSION_NOT_OPENED",
MediaDrm.ErrorCodes.ERROR_SESSION_NOT_OPENED, e.getErrorCode());
assertTrue("Expected ERROR_SESSION_NOT_OPENED value in info",
e.getDiagnosticInfo().contains(
String.valueOf(MediaDrm.ErrorCodes.ERROR_SESSION_NOT_OPENED)));
} finally {
if (drm != null) {
drm.close();
}
}
}
private String getClearkeyVersion(MediaDrm drm) {
try {
return drm.getPropertyString("version");
} catch (Exception e) {
return "unavailable";
}
}
private boolean cannotHandleGetPropertyByteArray(MediaDrm drm) {
boolean apiNotSupported = false;
byte[] bytes = new byte[0];
try {
bytes = drm.getPropertyByteArray(DEVICEID_PROPERTY_KEY);
} catch (IllegalArgumentException e) {
// Expected exception for invalid key or api not implemented
apiNotSupported = true;
}
return apiNotSupported;
}
private boolean cannotHandleSetPropertyString(MediaDrm drm) {
boolean apiNotSupported = false;
final byte[] bytes = new byte[0];
try {
drm.setPropertyString(LISTENER_TEST_SUPPORT_PROPERTY_KEY, "testing");
} catch (IllegalArgumentException e) {
// Expected exception for invalid key or api not implemented
apiNotSupported = true;
}
return apiNotSupported;
}
private boolean watchHasNoClearkeySupport() {
if (!MediaDrm.isCryptoSchemeSupported(CLEARKEY_SCHEME_UUID)) {
if (isWatchDevice()) {
return true;
} else {
throw new Error(ERR_MSG_CRYPTO_SCHEME_NOT_SUPPORTED);
}
}
return false;
}
private List<Surface> getSurfaces() {
return Arrays.asList(getActivity().getSurfaceHolder().getSurface());
}
}