From 74ad0949fd09be26bfc60702794d3eb6e8a213e1 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 19 Feb 2021 12:52:46 +0000 Subject: [PATCH] Add support for preparing DRM sessions before they're needed This adds an optional DrmSessionManager#preacquireSession() method and implements it on DefaultDrmSessionManager. The manager doesn't promise to keep the preacquired sessions alive, and will proactively release them if a ResourceBusyException suggests the device is running out of available sessions in the underlying framework. In a future change, SampleQueue will preacquire sessions on the loading thread and keep track of preacquired 'references', releasing them when seeking or clearing the queue. Issue: #4133 PiperOrigin-RevId: 358381616 --- .../drm/DefaultDrmSessionManager.java | 218 +++++++++++++++--- .../exoplayer2/drm/DrmSessionManager.java | 62 +++++ .../drm/DefaultDrmSessionManagerTest.java | 155 +++++++++++++ 3 files changed, 401 insertions(+), 34 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 10d6accc51..e0eb8a26aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -47,9 +47,16 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; -/** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ +/** + * A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. + * + *

This implementation supports pre-acquisition of sessions using {@link + * #preacquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}. + */ @RequiresApi(18) public class DefaultDrmSessionManager implements DrmSessionManager { @@ -282,14 +289,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager { private final List sessions; private final List provisioningSessions; + private final Set preacquiredSessionReferences; private final Set keepaliveSessions; private int prepareCallsCount; @Nullable private ExoMediaDrm exoMediaDrm; @Nullable private DefaultDrmSession placeholderDrmSession; @Nullable private DefaultDrmSession noMultiSessionDrmSession; - @Nullable private Looper playbackLooper; - private @MonotonicNonNull Handler sessionReleasingHandler; + private @MonotonicNonNull Looper playbackLooper; + private @MonotonicNonNull Handler playbackHandler; private int mode; @Nullable private byte[] offlineLicenseKeySetId; @@ -403,6 +411,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { mode = MODE_PLAYBACK; sessions = new ArrayList<>(); provisioningSessions = new ArrayList<>(); + preacquiredSessionReferences = Sets.newIdentityHashSet(); keepaliveSessions = Sets.newIdentityHashSet(); this.sessionKeepaliveMs = sessionKeepaliveMs; } @@ -466,10 +475,24 @@ public class DefaultDrmSessionManager implements DrmSessionManager { sessions.get(i).release(/* eventDispatcher= */ null); } } + releaseAllPreacquiredSessions(); + Assertions.checkNotNull(exoMediaDrm).release(); exoMediaDrm = null; } + @Override + public DrmSessionReference preacquireSession( + Looper playbackLooper, + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, + Format format) { + initPlaybackLooper(playbackLooper); + PreacquiredSessionReference preacquiredSessionReference = + new PreacquiredSessionReference(eventDispatcher); + preacquiredSessionReference.acquire(format); + return preacquiredSessionReference; + } + @Override @Nullable public DrmSession acquireSession( @@ -477,11 +500,27 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) { initPlaybackLooper(playbackLooper); + return acquireSession( + playbackLooper, + eventDispatcher, + format, + /* shouldReleasePreacquiredSessionsBeforeRetrying= */ true); + } + + // Must be called on the playback thread. + @Nullable + private DrmSession acquireSession( + Looper playbackLooper, + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, + Format format, + boolean shouldReleasePreacquiredSessionsBeforeRetrying) { maybeCreateMediaDrmHandler(playbackLooper); if (format.drmInitData == null) { // Content is not encrypted. - return maybeAcquirePlaceholderSession(MimeTypes.getTrackType(format.sampleMimeType)); + return maybeAcquirePlaceholderSession( + MimeTypes.getTrackType(format.sampleMimeType), + shouldReleasePreacquiredSessionsBeforeRetrying); } @Nullable List schemeDatas = null; @@ -515,7 +554,10 @@ public class DefaultDrmSessionManager implements DrmSessionManager { // Create a new session. session = createAndAcquireSessionWithRetry( - schemeDatas, /* isPlaceholderSession= */ false, eventDispatcher); + schemeDatas, + /* isPlaceholderSession= */ false, + eventDispatcher, + shouldReleasePreacquiredSessionsBeforeRetrying); if (!multiSession) { noMultiSessionDrmSession = session; } @@ -547,7 +589,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager { // Internal methods. @Nullable - private DrmSession maybeAcquirePlaceholderSession(int trackType) { + private DrmSession maybeAcquirePlaceholderSession( + int trackType, boolean shouldReleasePreacquiredSessionsBeforeRetrying) { ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); boolean avoidPlaceholderDrmSessions = FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) @@ -563,7 +606,8 @@ public class DefaultDrmSessionManager implements DrmSessionManager { createAndAcquireSessionWithRetry( /* schemeDatas= */ ImmutableList.of(), /* isPlaceholderSession= */ true, - /* eventDispatcher= */ null); + /* eventDispatcher= */ null, + shouldReleasePreacquiredSessionsBeforeRetrying); sessions.add(placeholderDrmSession); this.placeholderDrmSession = placeholderDrmSession; } else { @@ -607,12 +651,14 @@ public class DefaultDrmSessionManager implements DrmSessionManager { return true; } - private void initPlaybackLooper(Looper playbackLooper) { + @EnsuresNonNull({"this.playbackLooper", "this.playbackHandler"}) + private synchronized void initPlaybackLooper(Looper playbackLooper) { if (this.playbackLooper == null) { this.playbackLooper = playbackLooper; - this.sessionReleasingHandler = new Handler(playbackLooper); + this.playbackHandler = new Handler(playbackLooper); } else { Assertions.checkState(this.playbackLooper == playbackLooper); + Assertions.checkNotNull(playbackHandler); } } @@ -625,35 +671,68 @@ public class DefaultDrmSessionManager implements DrmSessionManager { private DefaultDrmSession createAndAcquireSessionWithRetry( @Nullable List schemeDatas, boolean isPlaceholderSession, - @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, + boolean shouldReleasePreacquiredSessionsBeforeRetrying) { DefaultDrmSession session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); - if (session.getState() == DrmSession.STATE_ERROR - && (Util.SDK_INT < 19 - || Assertions.checkNotNull(session.getError()).getCause() - instanceof ResourceBusyException)) { - // We're short on DRM session resources, so eagerly release all our keepalive sessions. - // ResourceBusyException is only available at API 19, so on earlier versions we always - // eagerly release regardless of the underlying error. - if (!keepaliveSessions.isEmpty()) { - // Make a local copy, because sessions are removed from this.keepaliveSessions during - // release (via callback). - ImmutableSet keepaliveSessions = - ImmutableSet.copyOf(this.keepaliveSessions); - for (DrmSession keepaliveSession : keepaliveSessions) { - keepaliveSession.release(/* eventDispatcher= */ null); - } - // Undo the acquisitions from createAndAcquireSession(). - session.release(eventDispatcher); - if (sessionKeepaliveMs != C.TIME_UNSET) { - session.release(/* eventDispatcher= */ null); - } - session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); + // If we're short on DRM session resources, first try eagerly releasing all our keepalive + // sessions and then retry the acquisition. + if (acquisitionFailedIndicatingResourceShortage(session) && !keepaliveSessions.isEmpty()) { + // Make a local copy, because sessions are removed from this.keepaliveSessions during + // release (via callback). + ImmutableSet keepaliveSessions = + ImmutableSet.copyOf(this.keepaliveSessions); + for (DrmSession keepaliveSession : keepaliveSessions) { + keepaliveSession.release(/* eventDispatcher= */ null); } + undoAcquisition(session, eventDispatcher); + session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); + } + + // If the acquisition failed again due to continued resource shortage, and + // shouldReleasePreacquiredSessionsBeforeRetrying is true, try releasing all pre-acquired + // sessions and then retry the acquisition. + if (acquisitionFailedIndicatingResourceShortage(session) + && shouldReleasePreacquiredSessionsBeforeRetrying + && !preacquiredSessionReferences.isEmpty()) { + releaseAllPreacquiredSessions(); + undoAcquisition(session, eventDispatcher); + session = createAndAcquireSession(schemeDatas, isPlaceholderSession, eventDispatcher); } return session; } + private static boolean acquisitionFailedIndicatingResourceShortage(DrmSession session) { + // ResourceBusyException is only available at API 19, so on earlier versions we + // assume any error indicates resource shortage (ensuring we retry). + return session.getState() == DrmSession.STATE_ERROR + && (Util.SDK_INT < 19 + || Assertions.checkNotNull(session.getError()).getCause() + instanceof ResourceBusyException); + } + + /** + * Undoes the acquisitions from {@link #createAndAcquireSession(List, boolean, + * DrmSessionEventListener.EventDispatcher)}. + */ + private void undoAcquisition( + DrmSession session, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { + session.release(eventDispatcher); + if (sessionKeepaliveMs != C.TIME_UNSET) { + session.release(/* eventDispatcher= */ null); + } + } + + private void releaseAllPreacquiredSessions() { + // Make a local copy, because sessions are removed from this.preacquiredSessionReferences + // during release (via callback). + ImmutableSet preacquiredSessionReferences = + ImmutableSet.copyOf(this.preacquiredSessionReferences); + for (PreacquiredSessionReference preacquiredSessionReference : preacquiredSessionReferences) { + preacquiredSessionReference.release(); + } + } + /** * Creates a new {@link DefaultDrmSession} and acquires it on behalf of the caller (passing in * {@code eventDispatcher}). @@ -782,7 +861,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { if (sessionKeepaliveMs != C.TIME_UNSET) { // The session has been acquired elsewhere so we want to cancel our timeout. keepaliveSessions.remove(session); - Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session); + Assertions.checkNotNull(playbackHandler).removeCallbacksAndMessages(session); } } @@ -791,7 +870,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { if (newReferenceCount == 1 && sessionKeepaliveMs != C.TIME_UNSET) { // Only the internal keep-alive reference remains, so we can start the timeout. keepaliveSessions.add(session); - Assertions.checkNotNull(sessionReleasingHandler) + Assertions.checkNotNull(playbackHandler) .postAtTime( () -> session.release(/* eventDispatcher= */ null), session, @@ -812,7 +891,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { } provisioningSessions.remove(session); if (sessionKeepaliveMs != C.TIME_UNSET) { - Assertions.checkNotNull(sessionReleasingHandler).removeCallbacksAndMessages(session); + Assertions.checkNotNull(playbackHandler).removeCallbacksAndMessages(session); keepaliveSessions.remove(session); } } @@ -827,4 +906,75 @@ public class DefaultDrmSessionManager implements DrmSessionManager { Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget(); } } + + /** + * An implementation of {@link DrmSessionReference} that lazily acquires the underlying {@link + * DrmSession}. + * + *

A new instance is needed for each reference (compared to maintaining exactly one instance + * for each {@link DrmSession}) because each associated {@link + * DrmSessionEventListener.EventDispatcher} might be different. The {@link + * DrmSessionEventListener.EventDispatcher} is required to implement the zero-arg {@link + * DrmSessionReference#release()} method. + */ + private class PreacquiredSessionReference implements DrmSessionReference { + + @Nullable private final DrmSessionEventListener.EventDispatcher eventDispatcher; + + @Nullable private DrmSession session; + private boolean isReleased; + + /** + * Constructs an instance. + * + * @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} passed to {@link + * #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)}. + */ + public PreacquiredSessionReference( + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { + this.eventDispatcher = eventDispatcher; + } + + /** + * Acquires the underlying session. + * + *

Must be called at most once. Can be called from any thread. + */ + @RequiresNonNull("playbackHandler") + public void acquire(Format format) { + playbackHandler.post( + () -> { + if (prepareCallsCount == 0 || isReleased) { + // The manager has been fully released or this reference has already been released. + // Abort the acquisition attempt. + return; + } + this.session = + acquireSession( + Assertions.checkNotNull(playbackLooper), + eventDispatcher, + format, + /* shouldReleasePreacquiredSessionsBeforeRetrying= */ false); + preacquiredSessionReferences.add(this); + }); + } + + @Override + public void release() { + // Ensure the underlying session is released immediately if we're already on the playback + // thread, to allow a failed session opening to be immediately retried. + Util.postOrRun( + Assertions.checkNotNull(playbackHandler), + () -> { + if (isReleased) { + return; + } + if (session != null) { + session.release(eventDispatcher); + } + preacquiredSessionReferences.remove(this); + isReleased = true; + }); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index 70dc4fa7f5..4b3ee553d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -22,6 +22,23 @@ import com.google.android.exoplayer2.Format; /** Manages a DRM session. */ public interface DrmSessionManager { + /** + * Represents a single reference count of a {@link DrmSession}, while deliberately not giving + * access to the underlying session. + */ + interface DrmSessionReference { + /** A reference that is never populated with an underlying {@link DrmSession}. */ + DrmSessionReference EMPTY = () -> {}; + + /** + * Releases the underlying session at most once. + * + *

Can be called from any thread. Calling this method more than once will only release the + * underlying session once. + */ + void release(); + } + /** An instance that supports no DRM schemes. */ DrmSessionManager DRM_UNSUPPORTED = new DrmSessionManager() { @@ -81,6 +98,51 @@ public interface DrmSessionManager { // Do nothing. } + /** + * Pre-acquires a DRM session for the specified {@link Format}. + * + *

This notifies the manager that a subsequent call to {@link #acquireSession(Looper, + * DrmSessionEventListener.EventDispatcher, Format)} with the same {@link Format} is likely, + * allowing a manager that supports pre-acquisition to get the required {@link DrmSession} ready + * in the background. + * + *

The caller must call {@link DrmSessionReference#release()} on the returned instance when + * they no longer require the pre-acquisition (i.e. they know they won't be making a matching call + * to {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} in the near + * future). + * + *

This manager may silently release the underlying session in order to allow another operation + * to complete. This will result in a subsequent call to {@link #acquireSession(Looper, + * DrmSessionEventListener.EventDispatcher, Format)} re-initializing a new session, including + * repeating key loads and other async initialization steps. + * + *

The caller must separately call {@link #acquireSession(Looper, + * DrmSessionEventListener.EventDispatcher, Format)} in order to obtain a session suitable for + * playback. The pre-acquired {@link DrmSessionReference} and full {@link DrmSession} instances + * are distinct. The caller must release both, and can release the {@link DrmSessionReference} + * before the {@link DrmSession} without affecting playback. + * + *

This can be called from any thread. + * + *

Implementations that do not support pre-acquisition always return an empty {@link + * DrmSessionReference} instance. + * + * @param playbackLooper The looper associated with the media playback thread. + * @param eventDispatcher The {@link DrmSessionEventListener.EventDispatcher} used to distribute + * events, and passed on to {@link + * DrmSession#acquire(DrmSessionEventListener.EventDispatcher)}. + * @param format The {@link Format} for which to pre-acquire a {@link DrmSession}. + * @return A releaser for the pre-acquired session. Guaranteed to be non-null even if the matching + * {@link #acquireSession(Looper, DrmSessionEventListener.EventDispatcher, Format)} would + * return null. + */ + default DrmSessionReference preacquireSession( + Looper playbackLooper, + @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, + Format format) { + return DrmSessionReference.EMPTY; + } + /** * Returns a {@link DrmSession} for the specified {@link Format}, with an incremented reference * count. May return null if the {@link Format#drmInitData} is null and the DRM session manager is diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java index c0b83e7a65..07398e0bf1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManagerTest.java @@ -20,14 +20,18 @@ import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; import android.os.Looper; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.FakeExoMediaDrm; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.shadows.ShadowLooper; @@ -38,6 +42,7 @@ import org.robolectric.shadows.ShadowLooper; // - Multiple acquisitions & releases for same keys -> multiple requests. // - Provisioning. // - Key denial. +// - Handling of ResourceBusyException (indicating session scarcity). @RunWith(AndroidJUnit4.class) public class DefaultDrmSessionManagerTest { @@ -252,6 +257,156 @@ public class DefaultDrmSessionManagerTest { assertThat(secondDrmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); } + @Test(timeout = 10_000) + public void preacquireSession_loadsKeysBeforeFullAcquisition() throws Exception { + AtomicInteger keyLoadCount = new AtomicInteger(0); + DrmSessionEventListener.EventDispatcher eventDispatcher = + new DrmSessionEventListener.EventDispatcher(); + eventDispatcher.addEventListener( + Util.createHandlerForCurrentLooper(), + new DrmSessionEventListener() { + @Override + public void onDrmKeysLoaded( + int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { + keyLoadCount.incrementAndGet(); + } + }); + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); + DrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm()) + // Disable keepalive + .setSessionKeepaliveMs(C.TIME_UNSET) + .build(/* mediaDrmCallback= */ licenseServer); + + drmSessionManager.prepare(); + + DrmSessionManager.DrmSessionReference sessionReference = + drmSessionManager.preacquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + eventDispatcher, + FORMAT_WITH_DRM_INIT_DATA); + + // Wait for the key load event to propagate, indicating the pre-acquired session is in + // STATE_OPENED_WITH_KEYS. + while (keyLoadCount.get() == 0) { + // Allow the key response to be handled. + ShadowLooper.idleMainLooper(); + } + + DrmSession drmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + + // Without idling the main/playback looper, we assert the session is already in OPENED_WITH_KEYS + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); + assertThat(keyLoadCount.get()).isEqualTo(1); + + // After releasing our concrete session reference, the session is held open by the pre-acquired + // reference. + drmSession.release(/* eventDispatcher= */ null); + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS); + + // Releasing the pre-acquired reference allows the session to be fully released. + sessionReference.release(); + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED); + } + + @Test(timeout = 10_000) + public void + preacquireSession_releaseBeforeUnderlyingAcquisitionCompletesReleasesSessionOnceAcquired() + throws Exception { + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); + DrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm()) + // Disable keepalive + .setSessionKeepaliveMs(C.TIME_UNSET) + .build(/* mediaDrmCallback= */ licenseServer); + + drmSessionManager.prepare(); + + DrmSessionManager.DrmSessionReference sessionReference = + drmSessionManager.preacquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA); + + // Release the pre-acquired reference before the underlying session has had a chance to be + // constructed. + sessionReference.release(); + + // Acquiring the same session triggers a second key load (because the pre-acquired session was + // fully released). + DrmSession drmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED); + + waitForOpenedWithKeys(drmSession); + + drmSession.release(/* eventDispatcher= */ null); + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED); + } + + @Test(timeout = 10_000) + public void preacquireSession_releaseManagerBeforeAcquisition_acquisitionDoesntHappen() + throws Exception { + FakeExoMediaDrm.LicenseServer licenseServer = + FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); + DrmSessionManager drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm()) + // Disable keepalive + .setSessionKeepaliveMs(C.TIME_UNSET) + .build(/* mediaDrmCallback= */ licenseServer); + + drmSessionManager.prepare(); + + DrmSessionManager.DrmSessionReference sessionReference = + drmSessionManager.preacquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA); + + // Release the manager before the underlying session has had a chance to be constructed. This + // will release all pre-acquired sessions. + drmSessionManager.release(); + + // Allow the acquisition event to be handled on the main/playback thread. + ShadowLooper.idleMainLooper(); + + // Re-prepare the manager so we can fully acquire the same session, and check the previous + // pre-acquisition didn't do anything. + drmSessionManager.prepare(); + DrmSession drmSession = + checkNotNull( + drmSessionManager.acquireSession( + /* playbackLooper= */ checkNotNull(Looper.myLooper()), + /* eventDispatcher= */ null, + FORMAT_WITH_DRM_INIT_DATA)); + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED); + waitForOpenedWithKeys(drmSession); + + drmSession.release(/* eventDispatcher= */ null); + // If the (still unreleased) pre-acquired session above was linked to the same underlying + // session then the state would still be OPENED_WITH_KEYS. + assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_RELEASED); + + // Release the pre-acquired session from above (this is a no-op, but we do it anyway for + // correctness). + sessionReference.release(); + drmSessionManager.release(); + } + private static void waitForOpenedWithKeys(DrmSession drmSession) { // Check the error first, so we get a meaningful failure if there's been an error. assertThat(drmSession.getError()).isNull();