| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.media; |
| |
| import android.media.MediaDrm; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.Callback; |
| import org.chromium.media.MediaDrmStorageBridge.PersistentInfo; |
| |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.UUID; |
| |
| /** |
| * The class manages relations among eme session ID, drm session ID and keyset |
| * ID. It also records the associated session information. |
| * |
| * For temporary session, it simply maintains the in memory map from session ID |
| * to related informations. When session is closed, the mapping is also removed. |
| * |
| * For persistent session, it also talks to persistent storage when loading |
| * information back to memory and updating changes to disk. |
| */ |
| class MediaDrmSessionManager { |
| /** |
| * The class groups drm session ID, eme session ID and key set ID. It hides |
| * the conversion among the three different IDs. |
| */ |
| static class SessionId { |
| private static final char[] HEX_CHAR_LOOKUP = "0123456789ABCDEF".toCharArray(); |
| |
| // ID used by browser and javascript to identify the session. It's |
| // the unique ID in EME world and also used as ID for this class. |
| // For temporary session, eme ID should match drm ID. For persistent |
| // session, EME ID is a random generated string because the persistent |
| // ID (key set ID) is generated much later than eme ID. |
| private final byte[] mEmeId; |
| |
| // Temporary ID used by MediaDrm session, returned by |
| // MediaDrm.openSession. |
| private byte[] mDrmId; |
| |
| // Persistent ID used by MediaDrm to identify persistent licenses, |
| // returned by MediaDrm.provideKeyResponse. |
| private byte[] mKeySetId; |
| |
| /** |
| * Convert byte array to hex string for logging. |
| * This is modified from BytesToHexString() in url/url_canon_unittest.cc. |
| */ |
| static String toHexString(byte[] bytes) { |
| StringBuilder hexString = new StringBuilder(); |
| for (int i = 0; i < bytes.length; ++i) { |
| hexString.append(HEX_CHAR_LOOKUP[bytes[i] >>> 4]); |
| hexString.append(HEX_CHAR_LOOKUP[bytes[i] & 0xf]); |
| } |
| return hexString.toString(); |
| } |
| |
| /** |
| * Create session ID with random generated (UUID.randomUUID) EME session ID. |
| * The ID must be unique within the origin of this object's Document over time, |
| * including across Documents and browsing sessions. |
| * https://w3c.github.io/encrypted-media/#dom-mediakeysession-generaterequest |
| * |
| * @param drmId Raw DRM ID created by MediaDrm. |
| * @return Session ID with random generated EME session ID. |
| */ |
| static SessionId createPersistentSessionId(byte[] drmId) { |
| byte[] emeId = ApiCompatibilityUtils.getBytesUtf8( |
| UUID.randomUUID().toString().replace('-', '0')); |
| return new SessionId(emeId, drmId, null /* keySetId */); |
| } |
| |
| /** |
| * Create session ID for temporary license session. The DRM session ID is |
| * used as EME session ID. |
| * |
| * @param drmIdAsEmeId Raw DRM session ID created by MediaDrm. |
| * @return Session ID with DRM session ID as EME session ID. |
| */ |
| static SessionId createTemporarySessionId(byte[] drmId) { |
| return new SessionId(drmId, drmId, null /* keySetId */); |
| } |
| |
| /** |
| * Create session ID used to report session doesn't exist. |
| */ |
| static SessionId createNoExistSessionId() { |
| return createTemporarySessionId(new byte[0]); |
| } |
| |
| private SessionId(byte[] emeId, byte[] drmId, byte[] keySetId) { |
| assert emeId != null; |
| assert drmId != null || keySetId != null; |
| |
| mEmeId = emeId; |
| mDrmId = drmId; |
| mKeySetId = keySetId; |
| } |
| |
| byte[] drmId() { |
| return mDrmId; |
| } |
| |
| byte[] emeId() { |
| return mEmeId; |
| } |
| |
| byte[] keySetId() { |
| return mKeySetId; |
| } |
| |
| private void setKeySetId(byte[] keySetId) { |
| mKeySetId = keySetId; |
| } |
| |
| private void setDrmId(byte[] drmId) { |
| mDrmId = drmId; |
| } |
| |
| boolean isEqual(SessionId that) { |
| return Arrays.equals(mEmeId, that.emeId()); |
| } |
| |
| String toHexString() { |
| return toHexString(mEmeId); |
| } |
| } |
| |
| static class SessionInfo { |
| private final SessionId mSessionId; |
| private final String mMimeType; |
| |
| // Key type of license in the session. It should be one of |
| // MediaDrm.KEY_TYPE_XXX. |
| private int mKeyType; |
| |
| private SessionInfo(SessionId sessionId, String mimeType, int keyType) { |
| assert sessionId != null; |
| assert mimeType != null && !mimeType.isEmpty(); |
| |
| mSessionId = sessionId; |
| mMimeType = mimeType; |
| mKeyType = keyType; |
| } |
| |
| String mimeType() { |
| return mMimeType; |
| } |
| |
| int keyType() { |
| return mKeyType; |
| } |
| |
| // Private methods that are visible in this file only. |
| |
| private SessionId sessionId() { |
| return mSessionId; |
| } |
| |
| private void setKeyType(int keyType) { |
| mKeyType = keyType; |
| } |
| |
| private PersistentInfo toPersistentInfo() { |
| assert mSessionId.keySetId() != null; |
| |
| return new PersistentInfo( |
| mSessionId.emeId(), mSessionId.keySetId(), mMimeType, mKeyType); |
| } |
| |
| private static SessionInfo fromPersistentInfo(PersistentInfo persistentInfo) { |
| assert persistentInfo != null; |
| assert persistentInfo.emeId() != null; |
| assert persistentInfo.keySetId() != null; |
| |
| SessionId sessionId = new SessionId( |
| persistentInfo.emeId(), null /* drmId */, persistentInfo.keySetId()); |
| return new SessionInfo(sessionId, persistentInfo.mimeType(), |
| getKeyTypeFromPersistentInfo(persistentInfo)); |
| } |
| |
| private static int getKeyTypeFromPersistentInfo(PersistentInfo persistentInfo) { |
| int keyType = persistentInfo.keyType(); |
| if (keyType == MediaDrm.KEY_TYPE_OFFLINE || keyType == MediaDrm.KEY_TYPE_RELEASE) { |
| return keyType; |
| } |
| |
| // Key type is missing. Use OFFLINE by default. |
| return MediaDrm.KEY_TYPE_OFFLINE; |
| } |
| } |
| |
| // Maps from DRM/EME session ID to SessionInfo. SessionInfo contains |
| // SessionId, so that we can: |
| // 1. Get SessionInfo with EME/DRM session ID. |
| // 2. Get SessionId from EME/DRM session ID. |
| // 3. Get EME/DRM session ID from DRM/EME session ID. |
| // SessionId always has a valid EME session ID, so all opened session should |
| // have an entry in mEmeSessionInfoMap. |
| private HashMap<ByteBuffer, SessionInfo> mEmeSessionInfoMap; |
| private HashMap<ByteBuffer, SessionInfo> mDrmSessionInfoMap; |
| |
| // The persistent storage to record map from EME session ID to key set ID |
| // for persistent license. |
| private MediaDrmStorageBridge mStorage; |
| |
| public MediaDrmSessionManager(MediaDrmStorageBridge storage) { |
| mEmeSessionInfoMap = new HashMap<>(); |
| mDrmSessionInfoMap = new HashMap<>(); |
| |
| mStorage = storage; |
| } |
| |
| /** |
| * Set drm ID. It should only be called for persistent license session |
| * without an opened drm session. |
| */ |
| void setDrmId(SessionId sessionId, byte[] drmId) { |
| SessionInfo info = get(sessionId); |
| |
| assert info != null; |
| assert info.sessionId().isEqual(sessionId); |
| |
| sessionId.setDrmId(drmId); |
| mDrmSessionInfoMap.put(ByteBuffer.wrap(drmId), info); |
| } |
| |
| /** |
| * Set key set ID. It should only be called for persistent license session. |
| */ |
| void setKeySetId(SessionId sessionId, byte[] keySetId, Callback<Boolean> callback) { |
| assert get(sessionId) != null; |
| assert get(sessionId).keyType() == MediaDrm.KEY_TYPE_OFFLINE; |
| assert sessionId.keySetId() == null; |
| |
| sessionId.setKeySetId(keySetId); |
| |
| mStorage.saveInfo(get(sessionId).toPersistentInfo(), callback); |
| } |
| |
| /** |
| * Mark key as released. It should only be called for persistent license |
| * session. |
| */ |
| void setKeyType(SessionId sessionId, int keyType, Callback<Boolean> callback) { |
| SessionInfo info = get(sessionId); |
| |
| assert info != null; |
| |
| info.setKeyType(keyType); |
| mStorage.saveInfo(info.toPersistentInfo(), callback); |
| } |
| |
| /** |
| * Load |emeId|'s session data from persistent storage. |
| */ |
| void load(byte[] emeId, final Callback<SessionId> callback) { |
| mStorage.loadInfo(emeId, new Callback<PersistentInfo>() { |
| @Override |
| public void onResult(PersistentInfo persistentInfo) { |
| if (persistentInfo == null) { |
| callback.onResult(null); |
| return; |
| } |
| |
| // Loading same persistent license into different sessions isn't |
| // supported. |
| assert getSessionIdByEmeId(persistentInfo.emeId()) == null; |
| |
| SessionInfo info = SessionInfo.fromPersistentInfo(persistentInfo); |
| mEmeSessionInfoMap.put(ByteBuffer.wrap(persistentInfo.emeId()), info); |
| callback.onResult(info.sessionId()); |
| } |
| }); |
| } |
| |
| /** |
| * Remove persistent license info from persistent storage. |
| */ |
| void clearPersistentSessionInfo(SessionId sessionId, Callback<Boolean> callback) { |
| sessionId.setKeySetId(null); |
| mStorage.clearInfo(sessionId.emeId(), callback); |
| } |
| |
| /** |
| * Remove session and related infomration from memory, but doesn't touch |
| * persistent storage. |
| */ |
| void remove(SessionId sessionId) { |
| SessionInfo info = get(sessionId); |
| |
| assert info != null; |
| assert sessionId.isEqual(info.sessionId()); |
| |
| mEmeSessionInfoMap.remove(ByteBuffer.wrap(sessionId.emeId())); |
| if (sessionId.drmId() != null) { |
| mDrmSessionInfoMap.remove(ByteBuffer.wrap(sessionId.drmId())); |
| } |
| } |
| |
| List<SessionId> getAllSessionIds() { |
| ArrayList<SessionId> sessionIds = new ArrayList<>(); |
| for (SessionInfo info : mEmeSessionInfoMap.values()) { |
| sessionIds.add(info.sessionId()); |
| } |
| |
| return sessionIds; |
| } |
| |
| SessionInfo get(SessionId sessionId) { |
| return mEmeSessionInfoMap.get(ByteBuffer.wrap(sessionId.emeId())); |
| } |
| |
| void put(SessionId id, String mimeType, int keyType) { |
| SessionInfo info = new SessionInfo(id, mimeType, keyType); |
| mEmeSessionInfoMap.put(ByteBuffer.wrap(id.emeId()), info); |
| |
| if (id.drmId() != null) { |
| mDrmSessionInfoMap.put(ByteBuffer.wrap(id.drmId()), info); |
| } |
| } |
| |
| SessionId getSessionIdByEmeId(byte[] emeId) { |
| return getSessionIdFromMap(mEmeSessionInfoMap, emeId); |
| } |
| |
| SessionId getSessionIdByDrmId(byte[] drmId) { |
| return getSessionIdFromMap(mDrmSessionInfoMap, drmId); |
| } |
| |
| // Private methods |
| |
| private SessionId getSessionIdFromMap(HashMap<ByteBuffer, SessionInfo> map, byte[] id) { |
| SessionInfo info = map.get(ByteBuffer.wrap(id)); |
| if (info == null) { |
| return null; |
| } |
| |
| return info.sessionId(); |
| } |
| } |