From f6dc02fa6a977e7670a4111f105b37af89218150 Mon Sep 17 00:00:00 2001 From: Googler Date: Mon, 29 Jul 2024 01:41:00 -0700 Subject: [PATCH] Unsuppress/suppress playback on suitable media output updates PiperOrigin-RevId: 657111555 --- .../DefaultSuitableOutputChecker.java | 53 +++++++++++++------ .../media3/exoplayer/ExoPlayerImpl.java | 23 ++++++-- .../exoplayer/SuitableOutputChecker.java | 37 ++++++++++--- .../test/utils/FakeSuitableOutputChecker.java | 45 ++++++++++++---- 4 files changed, 122 insertions(+), 36 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultSuitableOutputChecker.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultSuitableOutputChecker.java index 0801594f3e..e0664fcf74 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultSuitableOutputChecker.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultSuitableOutputChecker.java @@ -15,15 +15,18 @@ */ package androidx.media3.exoplayer; -import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Assertions.checkStateNotNull; import android.content.Context; import android.media.MediaRoute2Info; import android.media.MediaRouter2; +import android.media.MediaRouter2.ControllerCallback; import android.media.MediaRouter2.RouteCallback; +import android.media.MediaRouter2.RoutingController; import android.media.RouteDiscoveryPreference; import android.media.RoutingSessionInfo; import android.os.Handler; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; @@ -42,12 +45,13 @@ import java.util.concurrent.Executor; private final RouteCallback routeCallback; private final Executor executor; - private boolean isEnabled; + @Nullable private ControllerCallback controllerCallback; + private boolean isPreviousSelectedOutputSuitableForPlayback; public DefaultSuitableOutputChecker(Context context, Handler eventHandler) { - this.router = MediaRouter2.getInstance(context); - this.routeCallback = new RouteCallback() {}; - this.executor = + router = MediaRouter2.getInstance(context); + routeCallback = new RouteCallback() {}; + executor = new Executor() { @Override public void execute(Runnable command) { @@ -57,19 +61,38 @@ import java.util.concurrent.Executor; } @Override - public void setEnabled(boolean isEnabled) { - if (isEnabled && !this.isEnabled) { - router.registerRouteCallback(executor, routeCallback, EMPTY_DISCOVERY_PREFERENCE); - this.isEnabled = true; - } else if (!isEnabled && this.isEnabled) { - router.unregisterRouteCallback(routeCallback); - this.isEnabled = false; - } + public void enable(Callback callback) { + router.registerRouteCallback(executor, routeCallback, EMPTY_DISCOVERY_PREFERENCE); + controllerCallback = + new ControllerCallback() { + @Override + public void onControllerUpdated(RoutingController controller) { + boolean isCurrentSelectedOutputSuitableForPlayback = + isSelectedOutputSuitableForPlayback(); + if (isPreviousSelectedOutputSuitableForPlayback + != isCurrentSelectedOutputSuitableForPlayback) { + isPreviousSelectedOutputSuitableForPlayback = + isCurrentSelectedOutputSuitableForPlayback; + callback.onSelectedOutputSuitabilityChanged( + isCurrentSelectedOutputSuitableForPlayback); + } + } + }; + router.registerControllerCallback(executor, controllerCallback); + isPreviousSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback(); } @Override - public boolean isSelectedRouteSuitableForPlayback() { - checkState(isEnabled, "SuitableOutputChecker is not enabled"); + public void disable() { + checkStateNotNull(controllerCallback, "SuitableOutputChecker is not enabled"); + router.unregisterControllerCallback(controllerCallback); + controllerCallback = null; + router.unregisterRouteCallback(routeCallback); + } + + @Override + public boolean isSelectedOutputSuitableForPlayback() { + checkStateNotNull(controllerCallback, "SuitableOutputChecker is not enabled"); int transferReason = router.getSystemController().getRoutingSessionInfo().getTransferReason(); boolean wasTransferInitiatedBySelf = router.getSystemController().wasTransferInitiatedBySelf(); for (MediaRoute2Info routeInfo : router.getSystemController().getSelectedRoutes()) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 1365e60eda..87abd7c4fb 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -404,7 +404,7 @@ import java.util.concurrent.TimeoutException; suitableOutputChecker = builder.suitableOutputChecker; if (suitableOutputChecker != null && Util.SDK_INT >= 35) { - suitableOutputChecker.setEnabled(true); + suitableOutputChecker.enable(this::onSelectedOutputSuitabilityChanged); } else if (suppressPlaybackOnUnsuitableOutput && Util.SDK_INT >= 23) { audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE); Api23.registerAudioDeviceCallback( @@ -1073,7 +1073,7 @@ import java.util.concurrent.TimeoutException; playbackInfo = playbackInfo.copyWithEstimatedPosition(); } if (suitableOutputChecker != null && Util.SDK_INT >= 35) { - suitableOutputChecker.setEnabled(false); + suitableOutputChecker.disable(); } playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE); playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(playbackInfo.periodId); @@ -2841,7 +2841,7 @@ import java.util.concurrent.TimeoutException; private boolean hasSupportedAudioOutput() { if (Util.SDK_INT >= 35 && suitableOutputChecker != null) { - return suitableOutputChecker.isSelectedRouteSuitableForPlayback(); + return suitableOutputChecker.isSelectedOutputSuitableForPlayback(); } else if (Util.SDK_INT >= 23 && audioManager != null) { return Api23.isSuitableExternalAudioOutputPresentInAudioDeviceInfoList( applicationContext, audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)); @@ -2954,6 +2954,23 @@ import java.util.concurrent.TimeoutException; /* ignored */ false); } + private void onSelectedOutputSuitabilityChanged(boolean isSelectedOutputSuitableForPlayback) { + if (isSelectedOutputSuitableForPlayback) { + if (playbackInfo.playbackSuppressionReason + == Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) { + updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates( + playbackInfo.playWhenReady, + PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + Player.PLAYBACK_SUPPRESSION_REASON_NONE); + } + } else { + updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates( + playbackInfo.playWhenReady, + PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT); + } + } + private static DeviceInfo createDeviceInfo(@Nullable StreamVolumeManager streamVolumeManager) { return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL) .setMinVolume(streamVolumeManager != null ? streamVolumeManager.getMinVolume() : 0) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SuitableOutputChecker.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SuitableOutputChecker.java index 04c7698d32..1bac5c6a8c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SuitableOutputChecker.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SuitableOutputChecker.java @@ -21,20 +21,43 @@ import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.media3.common.util.UnstableApi; -/** Provides methods to check the suitability of media outputs. */ +/** Provides methods to check the suitability of selected media outputs. */ @RequiresApi(35) @RestrictTo(LIBRARY_GROUP) @UnstableApi public interface SuitableOutputChecker { + + /** Callback to notify changes in the suitability of the selected media output. */ + interface Callback { + + /** + * Called when suitability of the selected output has changed. + * + * @param isSelectedOutputSuitableForPlayback true when selected output is suitable for + * playback. + */ + void onSelectedOutputSuitabilityChanged(boolean isSelectedOutputSuitableForPlayback); + } + /** - * Enables the current instance to receive updates on the suitable media outputs. + * Enables the current instance to receive updates on the selected media outputs and sets the + * {@link Callback} to notify the updates on the suitability of the selected output. * - *

When the caller no longer requires updated information, they must call this method with - * {@code false}. + *

When the caller no longer requires updates on suitable outputs, they must call {@link + * #disable()}. * - * @param isEnabled True if this instance should receive the updates. + * @param callback To receive notifications of changes in suitable media output changes. */ - void setEnabled(boolean isEnabled); + void enable(Callback callback); + + /** + * Disables the current instance to receive updates on the selected media outputs and clears the + * {@link Callback}. + * + * @throws IllegalStateException if this instance is not enabled to receive the updates on + * suitable media outputs. + */ + void disable(); /** * Returns whether any audio output is suitable for the media playback. @@ -42,5 +65,5 @@ public interface SuitableOutputChecker { * @throws IllegalStateException if this instance is not enabled to receive the updates on * suitable media outputs. */ - boolean isSelectedRouteSuitableForPlayback(); + boolean isSelectedOutputSuitableForPlayback(); } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeSuitableOutputChecker.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeSuitableOutputChecker.java index d44496ce8e..21268d95b3 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeSuitableOutputChecker.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeSuitableOutputChecker.java @@ -16,8 +16,9 @@ package androidx.media3.test.utils; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; -import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Assertions.checkStateNotNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.media3.common.util.UnstableApi; @@ -37,7 +38,7 @@ public final class FakeSuitableOutputChecker implements SuitableOutputChecker { /** * Sets the initial value to be returned from {@link - * SuitableOutputChecker#isSelectedRouteSuitableForPlayback()}. The default value is false. + * SuitableOutputChecker#isSelectedOutputSuitableForPlayback()}. The default value is false. */ @CanIgnoreReturnValue public Builder setIsSuitableExternalOutputAvailable(boolean isSuitableOutputAvailable) { @@ -55,21 +56,43 @@ public final class FakeSuitableOutputChecker implements SuitableOutputChecker { } } - private final boolean isSuitableOutputAvailable; - private boolean isEnabled; + private boolean isSelectedOutputSuitableForPlayback; + private boolean previousSelectedOutputSuitableForPlayback; + @Nullable private Callback callback; - public FakeSuitableOutputChecker(boolean isSuitableOutputAvailable) { - this.isSuitableOutputAvailable = isSuitableOutputAvailable; + public FakeSuitableOutputChecker(boolean isSelectedOutputSuitableForPlayback) { + this.isSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback; + this.previousSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback; } @Override - public void setEnabled(boolean isEnabled) { - this.isEnabled = isEnabled; + public void enable(Callback callback) { + this.callback = callback; } @Override - public boolean isSelectedRouteSuitableForPlayback() { - checkState(isEnabled, "SuitableOutputChecker is not enabled"); - return isSuitableOutputAvailable; + public void disable() { + this.callback = null; + } + + @Override + public boolean isSelectedOutputSuitableForPlayback() { + checkStateNotNull(callback, "SuitableOutputChecker is not enabled"); + return isSelectedOutputSuitableForPlayback; + } + + /** + * Updates the value to be returned by {@link + * SuitableOutputChecker#isSelectedOutputSuitableForPlayback()} and send callbacks to registered + * callers via {@link Callback#onSelectedOutputSuitabilityChanged(boolean)}. + */ + public void updateIsSelectedSuitableOutputAvailableAndNotify( + boolean isSelectedOutputSuitableForPlayback) { + this.isSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback; + if (callback != null + && previousSelectedOutputSuitableForPlayback != isSelectedOutputSuitableForPlayback) { + callback.onSelectedOutputSuitabilityChanged(isSelectedOutputSuitableForPlayback); + previousSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback; + } } }