1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.mozilla.thirdparty.com.google.android.exoplayer2.drm;
17 
18 import android.annotation.SuppressLint;
19 import android.annotation.TargetApi;
20 import android.os.Handler;
21 import android.os.Looper;
22 import android.os.Message;
23 import androidx.annotation.IntDef;
24 import androidx.annotation.Nullable;
25 import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
26 import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
27 import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
28 import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
29 import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
30 import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
31 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
32 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.EventDispatcher;
33 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
34 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
35 import java.lang.annotation.Documented;
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.UUID;
44 
45 /** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */
46 @TargetApi(18)
47 public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T> {
48 
49   /**
50    * Builder for {@link DefaultDrmSessionManager} instances.
51    *
52    * <p>See {@link #Builder} for the list of default values.
53    */
54   public static final class Builder {
55 
56     private final HashMap<String, String> keyRequestParameters;
57     private UUID uuid;
58     private ExoMediaDrm.Provider<ExoMediaCrypto> exoMediaDrmProvider;
59     private boolean multiSession;
60     private int[] useDrmSessionsForClearContentTrackTypes;
61     private boolean playClearSamplesWithoutKeys;
62     private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
63 
64     /**
65      * Creates a builder with default values. The default values are:
66      *
67      * <ul>
68      *   <li>{@link #setKeyRequestParameters keyRequestParameters}: An empty map.
69      *   <li>{@link #setUuidAndExoMediaDrmProvider UUID}: {@link C#WIDEVINE_UUID}.
70      *   <li>{@link #setUuidAndExoMediaDrmProvider ExoMediaDrm.Provider}: {@link
71      *       FrameworkMediaDrm#DEFAULT_PROVIDER}.
72      *   <li>{@link #setMultiSession multiSession}: {@code false}.
73      *   <li>{@link #setUseDrmSessionsForClearContent useDrmSessionsForClearContent}: No tracks.
74      *   <li>{@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code false}.
75      *   <li>{@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link
76      *       DefaultLoadErrorHandlingPolicy}.
77      * </ul>
78      */
79     @SuppressWarnings("unchecked")
Builder()80     public Builder() {
81       keyRequestParameters = new HashMap<>();
82       uuid = C.WIDEVINE_UUID;
83       exoMediaDrmProvider = (ExoMediaDrm.Provider) FrameworkMediaDrm.DEFAULT_PROVIDER;
84       loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
85       useDrmSessionsForClearContentTrackTypes = new int[0];
86     }
87 
88     /**
89      * Sets the key request parameters to pass as the last argument to {@link
90      * ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}.
91      *
92      * <p>Custom data for PlayReady should be set under {@link #PLAYREADY_CUSTOM_DATA_KEY}.
93      *
94      * @param keyRequestParameters A map with parameters.
95      * @return This builder.
96      */
setKeyRequestParameters(Map<String, String> keyRequestParameters)97     public Builder setKeyRequestParameters(Map<String, String> keyRequestParameters) {
98       this.keyRequestParameters.clear();
99       this.keyRequestParameters.putAll(Assertions.checkNotNull(keyRequestParameters));
100       return this;
101     }
102 
103     /**
104      * Sets the UUID of the DRM scheme and the {@link ExoMediaDrm.Provider} to use.
105      *
106      * @param uuid The UUID of the DRM scheme.
107      * @param exoMediaDrmProvider The {@link ExoMediaDrm.Provider}.
108      * @return This builder.
109      */
110     @SuppressWarnings({"rawtypes", "unchecked"})
setUuidAndExoMediaDrmProvider( UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider)111     public Builder setUuidAndExoMediaDrmProvider(
112         UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) {
113       this.uuid = Assertions.checkNotNull(uuid);
114       this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider);
115       return this;
116     }
117 
118     /**
119      * Sets whether this session manager is allowed to acquire multiple simultaneous sessions.
120      *
121      * <p>Users should pass false when a single key request will obtain all keys required to decrypt
122      * the associated content. {@code multiSession} is required when content uses key rotation.
123      *
124      * @param multiSession Whether this session manager is allowed to acquire multiple simultaneous
125      *     sessions.
126      * @return This builder.
127      */
setMultiSession(boolean multiSession)128     public Builder setMultiSession(boolean multiSession) {
129       this.multiSession = multiSession;
130       return this;
131     }
132 
133     /**
134      * Sets whether this session manager should attach {@link DrmSession DrmSessions} to the clear
135      * sections of the media content.
136      *
137      * <p>Using {@link DrmSession DrmSessions} for clear content avoids the recreation of decoders
138      * when transitioning between clear and encrypted sections of content.
139      *
140      * @param useDrmSessionsForClearContentTrackTypes The track types ({@link C#TRACK_TYPE_AUDIO}
141      *     and/or {@link C#TRACK_TYPE_VIDEO}) for which to use a {@link DrmSession} regardless of
142      *     whether the content is clear or encrypted.
143      * @return This builder.
144      * @throws IllegalArgumentException If {@code useDrmSessionsForClearContentTrackTypes} contains
145      *     track types other than {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_VIDEO}.
146      */
setUseDrmSessionsForClearContent( int... useDrmSessionsForClearContentTrackTypes)147     public Builder setUseDrmSessionsForClearContent(
148         int... useDrmSessionsForClearContentTrackTypes) {
149       for (int trackType : useDrmSessionsForClearContentTrackTypes) {
150         Assertions.checkArgument(
151             trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO);
152       }
153       this.useDrmSessionsForClearContentTrackTypes =
154           useDrmSessionsForClearContentTrackTypes.clone();
155       return this;
156     }
157 
158     /**
159      * Sets whether clear samples within protected content should be played when keys for the
160      * encrypted part of the content have yet to be loaded.
161      *
162      * @param playClearSamplesWithoutKeys Whether clear samples within protected content should be
163      *     played when keys for the encrypted part of the content have yet to be loaded.
164      * @return This builder.
165      */
setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys)166     public Builder setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys) {
167       this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
168       return this;
169     }
170 
171     /**
172      * Sets the {@link LoadErrorHandlingPolicy} for key and provisioning requests.
173      *
174      * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}.
175      * @return This builder.
176      */
setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy)177     public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
178       this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy);
179       return this;
180     }
181 
182     /** Builds a {@link DefaultDrmSessionManager} instance. */
build(MediaDrmCallback mediaDrmCallback)183     public DefaultDrmSessionManager<ExoMediaCrypto> build(MediaDrmCallback mediaDrmCallback) {
184       return new DefaultDrmSessionManager<>(
185           uuid,
186           exoMediaDrmProvider,
187           mediaDrmCallback,
188           keyRequestParameters,
189           multiSession,
190           useDrmSessionsForClearContentTrackTypes,
191           playClearSamplesWithoutKeys,
192           loadErrorHandlingPolicy);
193     }
194   }
195 
196   /**
197    * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does
198    * not contain scheme data for the required UUID.
199    */
200   public static final class MissingSchemeDataException extends Exception {
201 
MissingSchemeDataException(UUID uuid)202     private MissingSchemeDataException(UUID uuid) {
203       super("Media does not support uuid: " + uuid);
204     }
205   }
206 
207   /**
208    * A key for specifying PlayReady custom data in the key request parameters passed to {@link
209    * Builder#setKeyRequestParameters(Map)}.
210    */
211   public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData";
212 
213   /**
214    * Determines the action to be done after a session acquired. One of {@link #MODE_PLAYBACK},
215    * {@link #MODE_QUERY}, {@link #MODE_DOWNLOAD} or {@link #MODE_RELEASE}.
216    */
217   @Documented
218   @Retention(RetentionPolicy.SOURCE)
219   @IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE})
220   public @interface Mode {}
221   /**
222    * Loads and refreshes (if necessary) a license for playback. Supports streaming and offline
223    * licenses.
224    */
225   public static final int MODE_PLAYBACK = 0;
226   /** Restores an offline license to allow its status to be queried. */
227   public static final int MODE_QUERY = 1;
228   /** Downloads an offline license or renews an existing one. */
229   public static final int MODE_DOWNLOAD = 2;
230   /** Releases an existing offline license. */
231   public static final int MODE_RELEASE = 3;
232   /** Number of times to retry for initial provisioning and key request for reporting error. */
233   public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3;
234 
235   private static final String TAG = "DefaultDrmSessionMgr";
236 
237   private final UUID uuid;
238   private final ExoMediaDrm.Provider<T> exoMediaDrmProvider;
239   private final MediaDrmCallback callback;
240   private final HashMap<String, String> keyRequestParameters;
241   private final EventDispatcher<DefaultDrmSessionEventListener> eventDispatcher;
242   private final boolean multiSession;
243   private final int[] useDrmSessionsForClearContentTrackTypes;
244   private final boolean playClearSamplesWithoutKeys;
245   private final ProvisioningManagerImpl provisioningManagerImpl;
246   private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
247 
248   private final List<DefaultDrmSession<T>> sessions;
249   private final List<DefaultDrmSession<T>> provisioningSessions;
250 
251   private int prepareCallsCount;
252   @Nullable private ExoMediaDrm<T> exoMediaDrm;
253   @Nullable private DefaultDrmSession<T> placeholderDrmSession;
254   @Nullable private DefaultDrmSession<T> noMultiSessionDrmSession;
255   @Nullable private Looper playbackLooper;
256   private int mode;
257   @Nullable private byte[] offlineLicenseKeySetId;
258 
259   /* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler;
260 
261   /**
262    * @param uuid The UUID of the drm scheme.
263    * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
264    * @param callback Performs key and provisioning requests.
265    * @param keyRequestParameters An optional map of parameters to pass as the last argument to
266    *     {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.
267    * @deprecated Use {@link Builder} instead.
268    */
269   @SuppressWarnings("deprecation")
270   @Deprecated
DefaultDrmSessionManager( UUID uuid, ExoMediaDrm<T> exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap<String, String> keyRequestParameters)271   public DefaultDrmSessionManager(
272       UUID uuid,
273       ExoMediaDrm<T> exoMediaDrm,
274       MediaDrmCallback callback,
275       @Nullable HashMap<String, String> keyRequestParameters) {
276     this(
277         uuid,
278         exoMediaDrm,
279         callback,
280         keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,
281         /* multiSession= */ false,
282         INITIAL_DRM_REQUEST_RETRY_COUNT);
283   }
284 
285   /**
286    * @param uuid The UUID of the drm scheme.
287    * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
288    * @param callback Performs key and provisioning requests.
289    * @param keyRequestParameters An optional map of parameters to pass as the last argument to
290    *     {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.
291    * @param multiSession A boolean that specify whether multiple key session support is enabled.
292    *     Default is false.
293    * @deprecated Use {@link Builder} instead.
294    */
295   @Deprecated
DefaultDrmSessionManager( UUID uuid, ExoMediaDrm<T> exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap<String, String> keyRequestParameters, boolean multiSession)296   public DefaultDrmSessionManager(
297       UUID uuid,
298       ExoMediaDrm<T> exoMediaDrm,
299       MediaDrmCallback callback,
300       @Nullable HashMap<String, String> keyRequestParameters,
301       boolean multiSession) {
302     this(
303         uuid,
304         exoMediaDrm,
305         callback,
306         keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,
307         multiSession,
308         INITIAL_DRM_REQUEST_RETRY_COUNT);
309   }
310 
311   /**
312    * @param uuid The UUID of the drm scheme.
313    * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
314    * @param callback Performs key and provisioning requests.
315    * @param keyRequestParameters An optional map of parameters to pass as the last argument to
316    *     {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null.
317    * @param multiSession A boolean that specify whether multiple key session support is enabled.
318    *     Default is false.
319    * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and
320    *     key request before reporting error.
321    * @deprecated Use {@link Builder} instead.
322    */
323   @Deprecated
DefaultDrmSessionManager( UUID uuid, ExoMediaDrm<T> exoMediaDrm, MediaDrmCallback callback, @Nullable HashMap<String, String> keyRequestParameters, boolean multiSession, int initialDrmRequestRetryCount)324   public DefaultDrmSessionManager(
325       UUID uuid,
326       ExoMediaDrm<T> exoMediaDrm,
327       MediaDrmCallback callback,
328       @Nullable HashMap<String, String> keyRequestParameters,
329       boolean multiSession,
330       int initialDrmRequestRetryCount) {
331     this(
332         uuid,
333         new ExoMediaDrm.AppManagedProvider<>(exoMediaDrm),
334         callback,
335         keyRequestParameters == null ? new HashMap<>() : keyRequestParameters,
336         multiSession,
337         /* useDrmSessionsForClearContentTrackTypes= */ new int[0],
338         /* playClearSamplesWithoutKeys= */ false,
339         new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount));
340   }
341 
342   // the constructor does not initialize fields: offlineLicenseKeySetId
343   @SuppressWarnings("nullness:initialization.fields.uninitialized")
DefaultDrmSessionManager( UUID uuid, ExoMediaDrm.Provider<T> exoMediaDrmProvider, MediaDrmCallback callback, HashMap<String, String> keyRequestParameters, boolean multiSession, int[] useDrmSessionsForClearContentTrackTypes, boolean playClearSamplesWithoutKeys, LoadErrorHandlingPolicy loadErrorHandlingPolicy)344   private DefaultDrmSessionManager(
345       UUID uuid,
346       ExoMediaDrm.Provider<T> exoMediaDrmProvider,
347       MediaDrmCallback callback,
348       HashMap<String, String> keyRequestParameters,
349       boolean multiSession,
350       int[] useDrmSessionsForClearContentTrackTypes,
351       boolean playClearSamplesWithoutKeys,
352       LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
353     Assertions.checkNotNull(uuid);
354     Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
355     this.uuid = uuid;
356     this.exoMediaDrmProvider = exoMediaDrmProvider;
357     this.callback = callback;
358     this.keyRequestParameters = keyRequestParameters;
359     this.eventDispatcher = new EventDispatcher<>();
360     this.multiSession = multiSession;
361     this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes;
362     this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
363     this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
364     provisioningManagerImpl = new ProvisioningManagerImpl();
365     mode = MODE_PLAYBACK;
366     sessions = new ArrayList<>();
367     provisioningSessions = new ArrayList<>();
368   }
369 
370   /**
371    * Adds a {@link DefaultDrmSessionEventListener} to listen to drm session events.
372    *
373    * @param handler A handler to use when delivering events to {@code eventListener}.
374    * @param eventListener A listener of events.
375    */
addListener(Handler handler, DefaultDrmSessionEventListener eventListener)376   public final void addListener(Handler handler, DefaultDrmSessionEventListener eventListener) {
377     eventDispatcher.addListener(handler, eventListener);
378   }
379 
380   /**
381    * Removes a {@link DefaultDrmSessionEventListener} from the list of drm session event listeners.
382    *
383    * @param eventListener The listener to remove.
384    */
removeListener(DefaultDrmSessionEventListener eventListener)385   public final void removeListener(DefaultDrmSessionEventListener eventListener) {
386     eventDispatcher.removeListener(eventListener);
387   }
388 
389   /**
390    * Sets the mode, which determines the role of sessions acquired from the instance. This must be
391    * called before {@link #acquireSession(Looper, DrmInitData)} or {@link
392    * #acquirePlaceholderSession} is called.
393    *
394    * <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
395    * required.
396    *
397    * <p>{@code mode} must be one of these:
398    *
399    * <ul>
400    *   <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
401    *       requested otherwise the offline license is restored.
402    *   <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license
403    *       is restored.
404    *   <li>{@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is
405    *       requested otherwise the offline license is renewed.
406    *   <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline
407    *       license is released.
408    * </ul>
409    *
410    * @param mode The mode to be set.
411    * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
412    */
setMode(@ode int mode, @Nullable byte[] offlineLicenseKeySetId)413   public void setMode(@Mode int mode, @Nullable byte[] offlineLicenseKeySetId) {
414     Assertions.checkState(sessions.isEmpty());
415     if (mode == MODE_QUERY || mode == MODE_RELEASE) {
416       Assertions.checkNotNull(offlineLicenseKeySetId);
417     }
418     this.mode = mode;
419     this.offlineLicenseKeySetId = offlineLicenseKeySetId;
420   }
421 
422   // DrmSessionManager implementation.
423 
424   @Override
prepare()425   public final void prepare() {
426     if (prepareCallsCount++ == 0) {
427       Assertions.checkState(exoMediaDrm == null);
428       exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid);
429       exoMediaDrm.setOnEventListener(new MediaDrmEventListener());
430     }
431   }
432 
433   @Override
release()434   public final void release() {
435     if (--prepareCallsCount == 0) {
436       Assertions.checkNotNull(exoMediaDrm).release();
437       exoMediaDrm = null;
438     }
439   }
440 
441   @Override
canAcquireSession(DrmInitData drmInitData)442   public boolean canAcquireSession(DrmInitData drmInitData) {
443     if (offlineLicenseKeySetId != null) {
444       // An offline license can be restored so a session can always be acquired.
445       return true;
446     }
447     List<SchemeData> schemeDatas = getSchemeDatas(drmInitData, uuid, true);
448     if (schemeDatas.isEmpty()) {
449       if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) {
450         // Assume scheme specific data will be added before the session is opened.
451         Log.w(
452             TAG, "DrmInitData only contains common PSSH SchemeData. Assuming support for: " + uuid);
453       } else {
454         // No data for this manager's scheme.
455         return false;
456       }
457     }
458     String schemeType = drmInitData.schemeType;
459     if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) {
460       // If there is no scheme information, assume patternless AES-CTR.
461       return true;
462     } else if (C.CENC_TYPE_cbc1.equals(schemeType)
463         || C.CENC_TYPE_cbcs.equals(schemeType)
464         || C.CENC_TYPE_cens.equals(schemeType)) {
465       // API support for AES-CBC and pattern encryption was added in API 24. However, the
466       // implementation was not stable until API 25.
467       return Util.SDK_INT >= 25;
468     }
469     // Unknown schemes, assume one of them is supported.
470     return true;
471   }
472 
473   @Override
474   @Nullable
acquirePlaceholderSession(Looper playbackLooper, int trackType)475   public DrmSession<T> acquirePlaceholderSession(Looper playbackLooper, int trackType) {
476     assertExpectedPlaybackLooper(playbackLooper);
477     ExoMediaDrm<T> exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm);
478     boolean avoidPlaceholderDrmSessions =
479         FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType())
480             && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC;
481     // Avoid attaching a session to sparse formats.
482     if (avoidPlaceholderDrmSessions
483         || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET
484         || exoMediaDrm.getExoMediaCryptoType() == null) {
485       return null;
486     }
487     maybeCreateMediaDrmHandler(playbackLooper);
488     if (placeholderDrmSession == null) {
489       DefaultDrmSession<T> placeholderDrmSession =
490           createNewDefaultSession(
491               /* schemeDatas= */ Collections.emptyList(), /* isPlaceholderSession= */ true);
492       sessions.add(placeholderDrmSession);
493       this.placeholderDrmSession = placeholderDrmSession;
494     }
495     placeholderDrmSession.acquire();
496     return placeholderDrmSession;
497   }
498 
499   @Override
acquireSession(Looper playbackLooper, DrmInitData drmInitData)500   public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
501     assertExpectedPlaybackLooper(playbackLooper);
502     maybeCreateMediaDrmHandler(playbackLooper);
503 
504     @Nullable List<SchemeData> schemeDatas = null;
505     if (offlineLicenseKeySetId == null) {
506       schemeDatas = getSchemeDatas(drmInitData, uuid, false);
507       if (schemeDatas.isEmpty()) {
508         final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
509         eventDispatcher.dispatch(listener -> listener.onDrmSessionManagerError(error));
510         return new ErrorStateDrmSession<>(new DrmSessionException(error));
511       }
512     }
513 
514     @Nullable DefaultDrmSession<T> session;
515     if (!multiSession) {
516       session = noMultiSessionDrmSession;
517     } else {
518       // Only use an existing session if it has matching init data.
519       session = null;
520       for (DefaultDrmSession<T> existingSession : sessions) {
521         if (Util.areEqual(existingSession.schemeDatas, schemeDatas)) {
522           session = existingSession;
523           break;
524         }
525       }
526     }
527 
528     if (session == null) {
529       // Create a new session.
530       session = createNewDefaultSession(schemeDatas, /* isPlaceholderSession= */ false);
531       if (!multiSession) {
532         noMultiSessionDrmSession = session;
533       }
534       sessions.add(session);
535     }
536     session.acquire();
537     return session;
538   }
539 
540   @Override
541   @Nullable
getExoMediaCryptoType(DrmInitData drmInitData)542   public Class<T> getExoMediaCryptoType(DrmInitData drmInitData) {
543     return canAcquireSession(drmInitData)
544         ? Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType()
545         : null;
546   }
547 
548   // Internal methods.
549 
assertExpectedPlaybackLooper(Looper playbackLooper)550   private void assertExpectedPlaybackLooper(Looper playbackLooper) {
551     Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper);
552     this.playbackLooper = playbackLooper;
553   }
554 
maybeCreateMediaDrmHandler(Looper playbackLooper)555   private void maybeCreateMediaDrmHandler(Looper playbackLooper) {
556     if (mediaDrmHandler == null) {
557       mediaDrmHandler = new MediaDrmHandler(playbackLooper);
558     }
559   }
560 
createNewDefaultSession( @ullable List<SchemeData> schemeDatas, boolean isPlaceholderSession)561   private DefaultDrmSession<T> createNewDefaultSession(
562       @Nullable List<SchemeData> schemeDatas, boolean isPlaceholderSession) {
563     Assertions.checkNotNull(exoMediaDrm);
564     // Placeholder sessions should always play clear samples without keys.
565     boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession;
566     return new DefaultDrmSession<>(
567         uuid,
568         exoMediaDrm,
569         /* provisioningManager= */ provisioningManagerImpl,
570         /* releaseCallback= */ this::onSessionReleased,
571         schemeDatas,
572         mode,
573         playClearSamplesWithoutKeys,
574         isPlaceholderSession,
575         offlineLicenseKeySetId,
576         keyRequestParameters,
577         callback,
578         Assertions.checkNotNull(playbackLooper),
579         eventDispatcher,
580         loadErrorHandlingPolicy);
581   }
582 
onSessionReleased(DefaultDrmSession<T> drmSession)583   private void onSessionReleased(DefaultDrmSession<T> drmSession) {
584     sessions.remove(drmSession);
585     if (placeholderDrmSession == drmSession) {
586       placeholderDrmSession = null;
587     }
588     if (noMultiSessionDrmSession == drmSession) {
589       noMultiSessionDrmSession = null;
590     }
591     if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) {
592       // Other sessions were waiting for the released session to complete a provision operation.
593       // We need to have one of those sessions perform the provision operation instead.
594       provisioningSessions.get(1).provision();
595     }
596     provisioningSessions.remove(drmSession);
597   }
598 
599   /**
600    * Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}.
601    *
602    * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}.
603    * @param uuid The UUID.
604    * @param allowMissingData Whether a {@link SchemeData} with null {@link SchemeData#data} may be
605    *     returned.
606    * @return The extracted {@link SchemeData} instances, or an empty list if no suitable data is
607    *     present.
608    */
getSchemeDatas( DrmInitData drmInitData, UUID uuid, boolean allowMissingData)609   private static List<SchemeData> getSchemeDatas(
610       DrmInitData drmInitData, UUID uuid, boolean allowMissingData) {
611     // Look for matching scheme data (matching the Common PSSH box for ClearKey).
612     List<SchemeData> matchingSchemeDatas = new ArrayList<>(drmInitData.schemeDataCount);
613     for (int i = 0; i < drmInitData.schemeDataCount; i++) {
614       SchemeData schemeData = drmInitData.get(i);
615       boolean uuidMatches =
616           schemeData.matches(uuid)
617               || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID));
618       if (uuidMatches && (schemeData.data != null || allowMissingData)) {
619         matchingSchemeDatas.add(schemeData);
620       }
621     }
622     return matchingSchemeDatas;
623   }
624 
625   @SuppressLint("HandlerLeak")
626   private class MediaDrmHandler extends Handler {
627 
MediaDrmHandler(Looper looper)628     public MediaDrmHandler(Looper looper) {
629       super(looper);
630     }
631 
632     @Override
handleMessage(Message msg)633     public void handleMessage(Message msg) {
634       byte[] sessionId = (byte[]) msg.obj;
635       if (sessionId == null) {
636         // The event is not associated with any particular session.
637         return;
638       }
639       for (DefaultDrmSession<T> session : sessions) {
640         if (session.hasSessionId(sessionId)) {
641           session.onMediaDrmEvent(msg.what);
642           return;
643         }
644       }
645     }
646   }
647 
648   private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager<T> {
649     @Override
provisionRequired(DefaultDrmSession<T> session)650     public void provisionRequired(DefaultDrmSession<T> session) {
651       if (provisioningSessions.contains(session)) {
652         // The session has already requested provisioning.
653         return;
654       }
655       provisioningSessions.add(session);
656       if (provisioningSessions.size() == 1) {
657         // This is the first session requesting provisioning, so have it perform the operation.
658         session.provision();
659       }
660     }
661 
662     @Override
onProvisionCompleted()663     public void onProvisionCompleted() {
664       for (DefaultDrmSession<T> session : provisioningSessions) {
665         session.onProvisionCompleted();
666       }
667       provisioningSessions.clear();
668     }
669 
670     @Override
onProvisionError(Exception error)671     public void onProvisionError(Exception error) {
672       for (DefaultDrmSession<T> session : provisioningSessions) {
673         session.onProvisionError(error);
674       }
675       provisioningSessions.clear();
676     }
677   }
678 
679   private class MediaDrmEventListener implements OnEventListener<T> {
680 
681     @Override
onEvent( ExoMediaDrm<? extends T> md, @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data)682     public void onEvent(
683         ExoMediaDrm<? extends T> md,
684         @Nullable byte[] sessionId,
685         int event,
686         int extra,
687         @Nullable byte[] data) {
688       Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget();
689     }
690   }
691 }
692