Add workarounds for NoSuchMethodError from DRM framework exceptions

Issue: androidx/media#1145

PiperOrigin-RevId: 613573868
(cherry picked from commit a604600126)
This commit is contained in:
ibaker 2024-03-07 07:13:01 -08:00 committed by SheenaChhabra
parent 2dc2e5daf1
commit e55621496c
6 changed files with 269 additions and 31 deletions

View file

@ -28,6 +28,11 @@
Google TV, and Lenovo M10 FHD Plus that causes 60fps H265 streams to be Google TV, and Lenovo M10 FHD Plus that causes 60fps H265 streams to be
marked as unsupported marked as unsupported
([#966](https://github.com/androidx/media/issues/966)). ([#966](https://github.com/androidx/media/issues/966)).
* DRM:
* Work around a `NoSuchMethodError` which can be thrown by the `MediaDrm`
framework instead of `ResourceBusyException` or
`NotProvisionedException` on some Android 14 devices
([#1145](https://github.com/androidx/media/issues/1145)).
* Effect: * Effect:
* Improved PQ to SDR tone-mapping by converting color spaces. * Improved PQ to SDR tone-mapping by converting color spaces.
* Session: * Session:

View file

@ -396,8 +396,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return true; return true;
} catch (NotProvisionedException e) { } catch (NotProvisionedException e) {
provisioningManager.provisionRequired(this); provisioningManager.provisionRequired(this);
} catch (Exception e) { } catch (Exception | NoSuchMethodError e) {
onError(e, DrmUtil.ERROR_SOURCE_EXO_MEDIA_DRM); // Work around b/291440132.
if (DrmUtil.isFailureToConstructNotProvisionedException(e)) {
provisioningManager.provisionRequired(this);
} else {
onError(e, DrmUtil.ERROR_SOURCE_EXO_MEDIA_DRM);
}
} }
return false; return false;
@ -474,7 +479,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
try { try {
mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId); mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId);
return true; return true;
} catch (Exception e) { } catch (Exception | NoSuchMethodError e) {
onError(e, DrmUtil.ERROR_SOURCE_EXO_MEDIA_DRM); onError(e, DrmUtil.ERROR_SOURCE_EXO_MEDIA_DRM);
} }
return false; return false;
@ -494,7 +499,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, keyRequestParameters); currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, keyRequestParameters);
Util.castNonNull(requestHandler) Util.castNonNull(requestHandler)
.post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry); .post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry);
} catch (Exception e) { } catch (Exception | NoSuchMethodError e) {
onKeysError(e, /* thrownByExoMediaDrm= */ true); onKeysError(e, /* thrownByExoMediaDrm= */ true);
} }
} }
@ -506,8 +511,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
currentKeyRequest = null; currentKeyRequest = null;
if (response instanceof Exception) { if (response instanceof Exception || response instanceof NoSuchMethodError) {
onKeysError((Exception) response, /* thrownByExoMediaDrm= */ false); onKeysError((Throwable) response, /* thrownByExoMediaDrm= */ false);
return; return;
} }
@ -528,7 +533,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
state = STATE_OPENED_WITH_KEYS; state = STATE_OPENED_WITH_KEYS;
dispatchEvent(DrmSessionEventListener.EventDispatcher::drmKeysLoaded); dispatchEvent(DrmSessionEventListener.EventDispatcher::drmKeysLoaded);
} }
} catch (Exception e) { } catch (Exception | NoSuchMethodError e) {
onKeysError(e, /* thrownByExoMediaDrm= */ true); onKeysError(e, /* thrownByExoMediaDrm= */ true);
} }
} }
@ -540,8 +545,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
} }
private void onKeysError(Exception e, boolean thrownByExoMediaDrm) { /**
if (e instanceof NotProvisionedException) { * @param e Must be an instance of either {@link Exception} or {@link Error}.
*/
private void onKeysError(Throwable e, boolean thrownByExoMediaDrm) {
if (e instanceof NotProvisionedException
|| DrmUtil.isFailureToConstructNotProvisionedException(e)) {
provisioningManager.provisionRequired(this); provisioningManager.provisionRequired(this);
} else { } else {
onError( onError(
@ -552,11 +561,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
} }
private void onError(Exception e, @DrmUtil.ErrorSource int errorSource) { /**
* @param e Must be an instance of either {@link Exception} or {@link Error}.
*/
private void onError(Throwable e, @DrmUtil.ErrorSource int errorSource) {
lastException = lastException =
new DrmSessionException(e, DrmUtil.getErrorCodeForMediaDrmException(e, errorSource)); new DrmSessionException(e, DrmUtil.getErrorCodeForMediaDrmException(e, errorSource));
Log.e(TAG, "DRM session error", e); Log.e(TAG, "DRM session error", e);
dispatchEvent(eventDispatcher -> eventDispatcher.drmSessionManagerError(e)); if (e instanceof Exception) {
dispatchEvent(eventDispatcher -> eventDispatcher.drmSessionManagerError((Exception) e));
} else if (e instanceof Error) {
// Re-throw all Error types except a NoSuchMethodError caused by b/291440132.
if (!DrmUtil.isFailureToConstructResourceBusyException(e)
&& !DrmUtil.isFailureToConstructNotProvisionedException(e)) {
throw (Error) e;
}
} else {
throw new IllegalStateException("Unexpected Throwable subclass", e);
}
if (state != STATE_OPENED_WITH_KEYS) { if (state != STATE_OPENED_WITH_KEYS) {
state = STATE_ERROR; state = STATE_ERROR;
} }

View file

@ -657,11 +657,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
} }
private static boolean acquisitionFailedIndicatingResourceShortage(DrmSession session) { private static boolean acquisitionFailedIndicatingResourceShortage(DrmSession session) {
if (session.getState() != DrmSession.STATE_ERROR) {
return false;
}
@Nullable Throwable cause = checkNotNull(session.getError()).getCause();
// ResourceBusyException is only available at API 19, so on earlier versions we // ResourceBusyException is only available at API 19, so on earlier versions we
// assume any error indicates resource shortage (ensuring we retry). // assume any error indicates resource shortage (ensuring we retry).
return session.getState() == DrmSession.STATE_ERROR return Util.SDK_INT < 19 || cause instanceof ResourceBusyException
&& (Util.SDK_INT < 19 || DrmUtil.isFailureToConstructResourceBusyException(cause);
|| checkNotNull(session.getError()).getCause() instanceof ResourceBusyException);
} }
/** /**

View file

@ -25,6 +25,7 @@ import android.media.DeniedByServerException;
import android.media.MediaDrm; import android.media.MediaDrm;
import android.media.MediaDrmResetException; import android.media.MediaDrmResetException;
import android.media.NotProvisionedException; import android.media.NotProvisionedException;
import android.media.ResourceBusyException;
import androidx.annotation.DoNotInline; import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -75,12 +76,13 @@ public final class DrmUtil {
* exception. * exception.
*/ */
public static @PlaybackException.ErrorCode int getErrorCodeForMediaDrmException( public static @PlaybackException.ErrorCode int getErrorCodeForMediaDrmException(
Exception exception, @ErrorSource int errorSource) { Throwable exception, @ErrorSource int errorSource) {
if (Util.SDK_INT >= 21 && Api21.isMediaDrmStateException(exception)) { if (Util.SDK_INT >= 21 && Api21.isMediaDrmStateException(exception)) {
return Api21.mediaDrmStateExceptionToErrorCode(exception); return Api21.mediaDrmStateExceptionToErrorCode(exception);
} else if (Util.SDK_INT >= 23 && Api23.isMediaDrmResetException(exception)) { } else if (Util.SDK_INT >= 23 && Api23.isMediaDrmResetException(exception)) {
return PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR; return PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR;
} else if (Util.SDK_INT >= 18 && Api18.isNotProvisionedException(exception)) { } else if ((Util.SDK_INT >= 18 && Api18.isNotProvisionedException(exception))
|| isFailureToConstructNotProvisionedException(exception)) {
return PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED; return PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED;
} else if (Util.SDK_INT >= 18 && Api18.isDeniedByServerException(exception)) { } else if (Util.SDK_INT >= 18 && Api18.isDeniedByServerException(exception)) {
return PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED; return PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED;
@ -104,6 +106,28 @@ public final class DrmUtil {
} }
} }
/**
* Returns true if {@code e} represents a failure to construct a {@link NotProvisionedException}.
* See b/291440132.
*/
public static boolean isFailureToConstructNotProvisionedException(@Nullable Throwable e) {
return Util.SDK_INT == 34
&& e instanceof NoSuchMethodError
&& e.getMessage() != null
&& e.getMessage().contains("Landroid/media/NotProvisionedException;.<init>(");
}
/**
* Returns true if {@code e} represents a failure to construct a {@link ResourceBusyException}.
* See b/291440132.
*/
public static boolean isFailureToConstructResourceBusyException(@Nullable Throwable e) {
return Util.SDK_INT == 34
&& e instanceof NoSuchMethodError
&& e.getMessage() != null
&& e.getMessage().contains("Landroid/media/ResourceBusyException;.<init>(");
}
// Internal classes. // Internal classes.
@RequiresApi(18) @RequiresApi(18)

View file

@ -40,6 +40,7 @@ import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper; import org.robolectric.shadows.ShadowLooper;
/** Tests for {@link DefaultDrmSessionManager} and {@link DefaultDrmSession}. */ /** Tests for {@link DefaultDrmSessionManager} and {@link DefaultDrmSession}. */
@ -258,6 +259,21 @@ public class DefaultDrmSessionManagerTest {
@Test(timeout = 10_000) @Test(timeout = 10_000)
public void maxConcurrentSessionsExceeded_allKeepAliveSessionsEagerlyReleased() throws Exception { public void maxConcurrentSessionsExceeded_allKeepAliveSessionsEagerlyReleased() throws Exception {
maxConcurrentSessionsExceededAllKeepAliveSessionsEagerlyReleased(
/* throwNoSuchMethodErrorForResourceBusy= */ false);
}
/** Testing workarounds for b/291440132. */
@Config(sdk = 34)
@Test(timeout = 10_000)
public void maxConcurrentSessionsExceeded_allKeepAliveSessionsEagerlyReleased_noSuchMethodError()
throws Exception {
maxConcurrentSessionsExceededAllKeepAliveSessionsEagerlyReleased(
/* throwNoSuchMethodErrorForResourceBusy= */ true);
}
private static void maxConcurrentSessionsExceededAllKeepAliveSessionsEagerlyReleased(
boolean throwNoSuchMethodErrorForResourceBusy) {
ImmutableList<DrmInitData.SchemeData> secondSchemeDatas = ImmutableList<DrmInitData.SchemeData> secondSchemeDatas =
ImmutableList.of(DRM_SCHEME_DATAS.get(0).copyWithData(TestUtil.createByteArray(4, 5, 6))); ImmutableList.of(DRM_SCHEME_DATAS.get(0).copyWithData(TestUtil.createByteArray(4, 5, 6)));
FakeExoMediaDrm.LicenseServer licenseServer = FakeExoMediaDrm.LicenseServer licenseServer =
@ -267,7 +283,13 @@ public class DefaultDrmSessionManagerTest {
DrmSessionManager drmSessionManager = DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder() new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider( .setUuidAndExoMediaDrmProvider(
DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm(/* maxConcurrentSessions= */ 1)) DRM_SCHEME_UUID,
uuid ->
new FakeExoMediaDrm.Builder()
.setMaxConcurrentSessions(1)
.throwNoSuchMethodErrorForProvisioningAndResourceBusy(
throwNoSuchMethodErrorForResourceBusy)
.build())
.setSessionKeepaliveMs(10_000) .setSessionKeepaliveMs(10_000)
.setMultiSession(true) .setMultiSession(true)
.build(/* mediaDrmCallback= */ licenseServer); .build(/* mediaDrmCallback= */ licenseServer);
@ -298,6 +320,23 @@ public class DefaultDrmSessionManagerTest {
@Test(timeout = 10_000) @Test(timeout = 10_000)
public void maxConcurrentSessionsExceeded_allPreacquiredAndKeepaliveSessionsEagerlyReleased() public void maxConcurrentSessionsExceeded_allPreacquiredAndKeepaliveSessionsEagerlyReleased()
throws Exception { throws Exception {
maxConcurrentSessionsExceededAllPreacquiredAndKeepaliveSessionsEagerlyReleased(
/* throwNoSuchMethodErrorForResourceBusy= */ false);
}
/** Testing workarounds for b/291440132. */
@Config(sdk = 34)
@Test(timeout = 10_000)
public void
maxConcurrentSessionsExceeded_allPreacquiredAndKeepaliveSessionsEagerlyReleased_noSuchMethodError()
throws Exception {
maxConcurrentSessionsExceededAllPreacquiredAndKeepaliveSessionsEagerlyReleased(
/* throwNoSuchMethodErrorForResourceBusy= */ true);
}
private static void
maxConcurrentSessionsExceededAllPreacquiredAndKeepaliveSessionsEagerlyReleased(
boolean throwNoSuchMethodErrorForResourceBusy) {
ImmutableList<DrmInitData.SchemeData> secondSchemeDatas = ImmutableList<DrmInitData.SchemeData> secondSchemeDatas =
ImmutableList.of(DRM_SCHEME_DATAS.get(0).copyWithData(TestUtil.createByteArray(4, 5, 6))); ImmutableList.of(DRM_SCHEME_DATAS.get(0).copyWithData(TestUtil.createByteArray(4, 5, 6)));
FakeExoMediaDrm.LicenseServer licenseServer = FakeExoMediaDrm.LicenseServer licenseServer =
@ -308,7 +347,12 @@ public class DefaultDrmSessionManagerTest {
new DefaultDrmSessionManager.Builder() new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider( .setUuidAndExoMediaDrmProvider(
DRM_SCHEME_UUID, DRM_SCHEME_UUID,
uuid -> new FakeExoMediaDrm.Builder().setMaxConcurrentSessions(1).build()) uuid ->
new FakeExoMediaDrm.Builder()
.setMaxConcurrentSessions(1)
.throwNoSuchMethodErrorForProvisioningAndResourceBusy(
throwNoSuchMethodErrorForResourceBusy)
.build())
.setSessionKeepaliveMs(10_000) .setSessionKeepaliveMs(10_000)
.setMultiSession(true) .setMultiSession(true)
.build(/* mediaDrmCallback= */ licenseServer); .build(/* mediaDrmCallback= */ licenseServer);
@ -606,6 +650,22 @@ public class DefaultDrmSessionManagerTest {
@Test @Test
public void public void
deviceNotProvisioned_exceptionThrownFromOpenSession_provisioningDoneAndOpenSessionRetried() { deviceNotProvisioned_exceptionThrownFromOpenSession_provisioningDoneAndOpenSessionRetried() {
deviceNotProvisionedExceptionThrownFromOpenSessionProvisioningDoneAndOpenSessionRetried(
/* throwNoSuchMethodErrorForNotProvisioned= */ false);
}
/** Testing workarounds for b/291440132. */
@Config(sdk = 34)
@Test
public void
deviceNotProvisioned_exceptionThrownFromOpenSession_provisioningDoneAndOpenSessionRetried_noSuchMethodError() {
deviceNotProvisionedExceptionThrownFromOpenSessionProvisioningDoneAndOpenSessionRetried(
/* throwNoSuchMethodErrorForNotProvisioned= */ true);
}
private static void
deviceNotProvisionedExceptionThrownFromOpenSessionProvisioningDoneAndOpenSessionRetried(
boolean throwNoSuchMethodErrorForNotProvisioned) {
FakeExoMediaDrm.LicenseServer licenseServer = FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
@ -613,7 +673,12 @@ public class DefaultDrmSessionManagerTest {
new DefaultDrmSessionManager.Builder() new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider( .setUuidAndExoMediaDrmProvider(
DRM_SCHEME_UUID, DRM_SCHEME_UUID,
uuid -> new FakeExoMediaDrm.Builder().setProvisionsRequired(1).build()) uuid ->
new FakeExoMediaDrm.Builder()
.setProvisionsRequired(1)
.throwNoSuchMethodErrorForProvisioningAndResourceBusy(
throwNoSuchMethodErrorForNotProvisioned)
.build())
.build(/* mediaDrmCallback= */ licenseServer); .build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
drmSessionManager.prepare(); drmSessionManager.prepare();
@ -635,6 +700,22 @@ public class DefaultDrmSessionManagerTest {
@Test @Test
public void public void
deviceNotProvisioned_exceptionThrownFromGetKeyRequest_provisioningDoneAndOpenSessionRetried() { deviceNotProvisioned_exceptionThrownFromGetKeyRequest_provisioningDoneAndOpenSessionRetried() {
deviceNotProvisionedExceptionThrownFromGetKeyRequestProvisioningDoneAndOpenSessionRetried(
/* throwNoSuchMethodErrorForNotProvisioned= */ false);
}
/** Testing workarounds for b/291440132. */
@Config(sdk = 34)
@Test
public void
deviceNotProvisioned_exceptionThrownFromGetKeyRequest_provisioningDoneAndOpenSessionRetried_noSuchMethodError() {
deviceNotProvisionedExceptionThrownFromGetKeyRequestProvisioningDoneAndOpenSessionRetried(
/* throwNoSuchMethodErrorForNotProvisioned= */ true);
}
private static void
deviceNotProvisionedExceptionThrownFromGetKeyRequestProvisioningDoneAndOpenSessionRetried(
boolean throwNoSuchMethodErrorForNotProvisioned) {
FakeExoMediaDrm.LicenseServer licenseServer = FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
@ -646,6 +727,8 @@ public class DefaultDrmSessionManagerTest {
new FakeExoMediaDrm.Builder() new FakeExoMediaDrm.Builder()
.setProvisionsRequired(1) .setProvisionsRequired(1)
.throwNotProvisionedExceptionFromGetKeyRequest() .throwNotProvisionedExceptionFromGetKeyRequest()
.throwNoSuchMethodErrorForProvisioningAndResourceBusy(
throwNoSuchMethodErrorForNotProvisioned)
.build()) .build())
.build(/* mediaDrmCallback= */ licenseServer); .build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
@ -665,6 +748,21 @@ public class DefaultDrmSessionManagerTest {
@Test @Test
public void deviceNotProvisioned_doubleProvisioningHandledAndOpenSessionRetried() { public void deviceNotProvisioned_doubleProvisioningHandledAndOpenSessionRetried() {
deviceNotProvisionedDoubleProvisioningHandledAndOpenSessionRetried(
/* throwNoSuchMethodErrorForNotProvisioned= */ false);
}
/** Testing workarounds for b/291440132. */
@Config(sdk = 34)
@Test
public void
deviceNotProvisioned_doubleProvisioningHandledAndOpenSessionRetried_noSuchMethodError() {
deviceNotProvisionedDoubleProvisioningHandledAndOpenSessionRetried(
/* throwNoSuchMethodErrorForNotProvisioned= */ true);
}
private static void deviceNotProvisionedDoubleProvisioningHandledAndOpenSessionRetried(
boolean throwNoSuchMethodErrorForNotProvisioned) {
FakeExoMediaDrm.LicenseServer licenseServer = FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
@ -672,7 +770,12 @@ public class DefaultDrmSessionManagerTest {
new DefaultDrmSessionManager.Builder() new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider( .setUuidAndExoMediaDrmProvider(
DRM_SCHEME_UUID, DRM_SCHEME_UUID,
uuid -> new FakeExoMediaDrm.Builder().setProvisionsRequired(2).build()) uuid ->
new FakeExoMediaDrm.Builder()
.setProvisionsRequired(2)
.throwNoSuchMethodErrorForProvisioningAndResourceBusy(
throwNoSuchMethodErrorForNotProvisioned)
.build())
.build(/* mediaDrmCallback= */ licenseServer); .build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
drmSessionManager.prepare(); drmSessionManager.prepare();
@ -693,6 +796,20 @@ public class DefaultDrmSessionManagerTest {
@Test @Test
public void keyResponseIndicatesProvisioningRequired_provisioningDone() { public void keyResponseIndicatesProvisioningRequired_provisioningDone() {
keyResponseIndicatesProvisioningRequiredProvisioningDone(
/* throwNoSuchMethodErrorForNotProvisioned= */ false);
}
/** Testing workarounds for b/291440132. */
@Config(sdk = 34)
@Test
public void keyResponseIndicatesProvisioningRequired_provisioningDone_noSuchMethodError() {
keyResponseIndicatesProvisioningRequiredProvisioningDone(
/* throwNoSuchMethodErrorForNotProvisioned= */ true);
}
private static void keyResponseIndicatesProvisioningRequiredProvisioningDone(
boolean throwNoSuchMethodErrorForNotProvisioned) {
FakeExoMediaDrm.LicenseServer licenseServer = FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.requiringProvisioningThenAllowingSchemeDatas( FakeExoMediaDrm.LicenseServer.requiringProvisioningThenAllowingSchemeDatas(
DRM_SCHEME_DATAS); DRM_SCHEME_DATAS);
@ -700,7 +817,12 @@ public class DefaultDrmSessionManagerTest {
DefaultDrmSessionManager drmSessionManager = DefaultDrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder() new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider( .setUuidAndExoMediaDrmProvider(
DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm.Builder().build()) DRM_SCHEME_UUID,
uuid ->
new FakeExoMediaDrm.Builder()
.throwNoSuchMethodErrorForProvisioningAndResourceBusy(
throwNoSuchMethodErrorForNotProvisioned)
.build())
.build(/* mediaDrmCallback= */ licenseServer); .build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
drmSessionManager.prepare(); drmSessionManager.prepare();
@ -719,10 +841,29 @@ public class DefaultDrmSessionManagerTest {
@Test @Test
public void provisioningUndoneWhileManagerIsActive_deviceReprovisioned() { public void provisioningUndoneWhileManagerIsActive_deviceReprovisioned() {
provisioningUndoneWhileManagerIsActiveDeviceReprovisioned(
/* throwNoSuchMethodErrorForNotProvisioned= */ false);
}
/** Testing workarounds for b/291440132. */
@Config(sdk = 34)
@Test
public void provisioningUndoneWhileManagerIsActive_deviceReprovisioned_noSuchMethodError() {
provisioningUndoneWhileManagerIsActiveDeviceReprovisioned(
/* throwNoSuchMethodErrorForNotProvisioned= */ true);
}
private static void provisioningUndoneWhileManagerIsActiveDeviceReprovisioned(
boolean throwNoSuchMethodErrorForNotProvisioned) {
FakeExoMediaDrm.LicenseServer licenseServer = FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS); FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
FakeExoMediaDrm mediaDrm = new FakeExoMediaDrm.Builder().setProvisionsRequired(2).build(); FakeExoMediaDrm mediaDrm =
new FakeExoMediaDrm.Builder()
.setProvisionsRequired(2)
.throwNoSuchMethodErrorForProvisioningAndResourceBusy(
throwNoSuchMethodErrorForNotProvisioned)
.build();
DefaultDrmSessionManager drmSessionManager = DefaultDrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder() new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(mediaDrm)) .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(mediaDrm))

View file

@ -16,6 +16,8 @@
package androidx.media3.test.utils; package androidx.media3.test.utils;
import static androidx.media3.common.util.Assertions.checkState;
import android.media.DeniedByServerException; import android.media.DeniedByServerException;
import android.media.MediaCryptoException; import android.media.MediaCryptoException;
import android.media.MediaDrmException; import android.media.MediaDrmException;
@ -72,6 +74,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
private int provisionsRequired; private int provisionsRequired;
private boolean throwNotProvisionedExceptionFromGetKeyRequest; private boolean throwNotProvisionedExceptionFromGetKeyRequest;
private int maxConcurrentSessions; private int maxConcurrentSessions;
private boolean throwNoSuchMethodErrorForProvisioningAndResourceBusy;
/** Constructs an instance. */ /** Constructs an instance. */
public Builder() { public Builder() {
@ -119,6 +122,26 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
return this; return this;
} }
/**
* Configures the {@link FakeExoMediaDrm} to throw {@link NoSuchMethodError} instead of {@link
* NotProvisionedException} or {@link ResourceBusyException}.
*
* <p>This simulates a framework bug (b/291440132) introduced in API 34 and resolved by
* http://r.android.com/2770659, allowing us to test workarounds for the bug.
*
* <p>The default is {@code false}.
*/
@CanIgnoreReturnValue
public Builder throwNoSuchMethodErrorForProvisioningAndResourceBusy(
boolean throwNoSuchMethodErrorForProvisioningAndResourceBusy) {
checkState(
!throwNoSuchMethodErrorForProvisioningAndResourceBusy || Util.SDK_INT == 34,
"The framework bug recreated by this method only exists on API 34.");
this.throwNoSuchMethodErrorForProvisioningAndResourceBusy =
throwNoSuchMethodErrorForProvisioningAndResourceBusy;
return this;
}
/** /**
* Sets the maximum number of concurrent sessions the {@link FakeExoMediaDrm} will support. * Sets the maximum number of concurrent sessions the {@link FakeExoMediaDrm} will support.
* *
@ -143,6 +166,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
enforceValidKeyResponses, enforceValidKeyResponses,
provisionsRequired, provisionsRequired,
throwNotProvisionedExceptionFromGetKeyRequest, throwNotProvisionedExceptionFromGetKeyRequest,
throwNoSuchMethodErrorForProvisioningAndResourceBusy,
maxConcurrentSessions); maxConcurrentSessions);
} }
} }
@ -170,6 +194,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
private final int provisionsRequired; private final int provisionsRequired;
private final int maxConcurrentSessions; private final int maxConcurrentSessions;
private final boolean throwNotProvisionedExceptionFromGetKeyRequest; private final boolean throwNotProvisionedExceptionFromGetKeyRequest;
private final boolean throwNoSuchMethodErrorForProvisioningAndResourceBusy;
private final Map<String, byte[]> byteProperties; private final Map<String, byte[]> byteProperties;
private final Map<String, String> stringProperties; private final Map<String, String> stringProperties;
private final Set<List<Byte>> openSessionIds; private final Set<List<Byte>> openSessionIds;
@ -184,12 +209,9 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
* @deprecated Use {@link Builder} instead. * @deprecated Use {@link Builder} instead.
*/ */
@Deprecated @Deprecated
@SuppressWarnings("deprecation") // Using deprecated constructor to reduce duplication.
public FakeExoMediaDrm() { public FakeExoMediaDrm() {
this( this(/* maxConcurrentSessions= */ Integer.MAX_VALUE);
/* enforceValidKeyResponses= */ true,
/* provisionsRequired= */ 0,
/* throwNotProvisionedExceptionFromGetKeyRequest= */ false,
/* maxConcurrentSessions= */ Integer.MAX_VALUE);
} }
/** /**
@ -201,6 +223,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
/* enforceValidKeyResponses= */ true, /* enforceValidKeyResponses= */ true,
/* provisionsRequired= */ 0, /* provisionsRequired= */ 0,
/* throwNotProvisionedExceptionFromGetKeyRequest= */ false, /* throwNotProvisionedExceptionFromGetKeyRequest= */ false,
/* throwNoSuchMethodErrorForProvisioningAndResourceBusy= */ false,
maxConcurrentSessions); maxConcurrentSessions);
} }
@ -208,12 +231,15 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
boolean enforceValidKeyResponses, boolean enforceValidKeyResponses,
int provisionsRequired, int provisionsRequired,
boolean throwNotProvisionedExceptionFromGetKeyRequest, boolean throwNotProvisionedExceptionFromGetKeyRequest,
boolean throwNoSuchMethodErrorForProvisioningAndResourceBusy,
int maxConcurrentSessions) { int maxConcurrentSessions) {
this.enforceValidKeyResponses = enforceValidKeyResponses; this.enforceValidKeyResponses = enforceValidKeyResponses;
this.provisionsRequired = provisionsRequired; this.provisionsRequired = provisionsRequired;
this.maxConcurrentSessions = maxConcurrentSessions; this.maxConcurrentSessions = maxConcurrentSessions;
this.throwNotProvisionedExceptionFromGetKeyRequest = this.throwNotProvisionedExceptionFromGetKeyRequest =
throwNotProvisionedExceptionFromGetKeyRequest; throwNotProvisionedExceptionFromGetKeyRequest;
this.throwNoSuchMethodErrorForProvisioningAndResourceBusy =
throwNoSuchMethodErrorForProvisioningAndResourceBusy;
byteProperties = new HashMap<>(); byteProperties = new HashMap<>();
stringProperties = new HashMap<>(); stringProperties = new HashMap<>();
openSessionIds = new HashSet<>(); openSessionIds = new HashSet<>();
@ -244,10 +270,16 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
public byte[] openSession() throws MediaDrmException { public byte[] openSession() throws MediaDrmException {
Assertions.checkState(referenceCount > 0); Assertions.checkState(referenceCount > 0);
if (!throwNotProvisionedExceptionFromGetKeyRequest && provisionsReceived < provisionsRequired) { if (!throwNotProvisionedExceptionFromGetKeyRequest && provisionsReceived < provisionsRequired) {
throw new NotProvisionedException("Not provisioned."); throwNotProvisionedException();
} }
if (openSessionIds.size() >= maxConcurrentSessions) { if (openSessionIds.size() >= maxConcurrentSessions) {
throw new ResourceBusyException("Too many sessions open. max=" + maxConcurrentSessions); if (throwNoSuchMethodErrorForProvisioningAndResourceBusy) {
throw new NoSuchMethodError(
"no non-static method"
+ " \"Landroid/media/ResourceBusyException;.<init>(Ljava/lang/String;III)V\"");
} else {
throw new ResourceBusyException("Too many sessions open. max=" + maxConcurrentSessions);
}
} }
byte[] sessionId = byte[] sessionId =
TestUtil.buildTestData(/* length= */ 10, sessionIdGenerator.incrementAndGet()); TestUtil.buildTestData(/* length= */ 10, sessionIdGenerator.incrementAndGet());
@ -280,7 +312,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
} }
Assertions.checkArgument(keyType == KEY_TYPE_STREAMING, "Unrecognised keyType: " + keyType); Assertions.checkArgument(keyType == KEY_TYPE_STREAMING, "Unrecognised keyType: " + keyType);
if (throwNotProvisionedExceptionFromGetKeyRequest && provisionsReceived < provisionsRequired) { if (throwNotProvisionedExceptionFromGetKeyRequest && provisionsReceived < provisionsRequired) {
throw new NotProvisionedException("Not provisioned."); throwNotProvisionedException();
} }
Assertions.checkState(openSessionIds.contains(toByteList(scope))); Assertions.checkState(openSessionIds.contains(toByteList(scope)));
Assertions.checkNotNull(schemeDatas); Assertions.checkNotNull(schemeDatas);
@ -306,7 +338,7 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
throw new DeniedByServerException("Key request denied"); throw new DeniedByServerException("Key request denied");
} }
if (responseAsList.equals(PROVISIONING_REQUIRED_RESPONSE)) { if (responseAsList.equals(PROVISIONING_REQUIRED_RESPONSE)) {
throw new NotProvisionedException("Provisioning required"); throwNotProvisionedException();
} }
if (enforceValidKeyResponses && !responseAsList.equals(VALID_KEY_RESPONSE)) { if (enforceValidKeyResponses && !responseAsList.equals(VALID_KEY_RESPONSE)) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
@ -451,6 +483,16 @@ public final class FakeExoMediaDrm implements ExoMediaDrm {
provisionsReceived = 0; provisionsReceived = 0;
} }
private void throwNotProvisionedException() throws NotProvisionedException {
if (throwNoSuchMethodErrorForProvisioningAndResourceBusy) {
throw new NoSuchMethodError(
"no non-static method"
+ " \"Landroid/media/NotProvisionedException;.<init>(Ljava/lang/String;III)V\"");
} else {
throw new NotProvisionedException("Not provisioned.");
}
}
private static ImmutableList<Byte> toByteList(byte[] byteArray) { private static ImmutableList<Byte> toByteList(byte[] byteArray) {
return ImmutableList.copyOf(Bytes.asList(byteArray)); return ImmutableList.copyOf(Bytes.asList(byteArray));
} }