mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Move DefaultSuitableOutputChecker operations to playback thread
PiperOrigin-RevId: 726879236 (cherry picked from commit e0ef6e51829dd1627d952d2677f532230cb2dbc9)
This commit is contained in:
parent
41af00f100
commit
8bd1db5f2c
@ -31,8 +31,11 @@ import android.media.MediaRouter2.RoutingController;
|
|||||||
import android.media.RouteDiscoveryPreference;
|
import android.media.RouteDiscoveryPreference;
|
||||||
import android.media.RoutingSessionInfo;
|
import android.media.RoutingSessionInfo;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.media3.common.util.BackgroundThreadStateHandler;
|
||||||
|
import androidx.media3.common.util.Clock;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
@ -43,26 +46,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
|
|
||||||
@Nullable private final SuitableOutputChecker impl;
|
@Nullable private final SuitableOutputChecker impl;
|
||||||
|
|
||||||
/**
|
/** Creates the default {@link SuitableOutputChecker}. */
|
||||||
* Creates the default {@link SuitableOutputChecker}.
|
public DefaultSuitableOutputChecker() {
|
||||||
*
|
|
||||||
* @param context A {@link Context}.
|
|
||||||
* @param eventHandler A {@link Handler} to trigger {@link Callback} methods on.
|
|
||||||
*/
|
|
||||||
public DefaultSuitableOutputChecker(Context context, Handler eventHandler) {
|
|
||||||
if (Util.SDK_INT >= 35) {
|
if (Util.SDK_INT >= 35) {
|
||||||
impl = new ImplApi35(context, eventHandler);
|
impl = new ImplApi35();
|
||||||
} else if (Util.SDK_INT >= 23) {
|
} else if (Util.SDK_INT >= 23) {
|
||||||
impl = new ImplApi23(context, eventHandler);
|
impl = new ImplApi23();
|
||||||
} else {
|
} else {
|
||||||
impl = null;
|
impl = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enable(Callback callback) {
|
public void enable(
|
||||||
|
Callback callback,
|
||||||
|
Context context,
|
||||||
|
Looper callbackLooper,
|
||||||
|
Looper backgroundLooper,
|
||||||
|
Clock clock) {
|
||||||
if (impl != null) {
|
if (impl != null) {
|
||||||
impl.enable(callback);
|
impl.enable(callback, context, callbackLooper, backgroundLooper, clock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,61 +88,63 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
/* preferredFeatures= */ ImmutableList.of(), /* activeScan= */ false)
|
/* preferredFeatures= */ ImmutableList.of(), /* activeScan= */ false)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
private final Context applicationContext;
|
|
||||||
private final Handler eventHandler;
|
|
||||||
|
|
||||||
private @MonotonicNonNull MediaRouter2 router;
|
private @MonotonicNonNull MediaRouter2 router;
|
||||||
private @MonotonicNonNull RouteCallback routeCallback;
|
private @MonotonicNonNull RouteCallback routeCallback;
|
||||||
@Nullable private ControllerCallback controllerCallback;
|
@Nullable private ControllerCallback controllerCallback;
|
||||||
private boolean isSelectedOutputSuitableForPlayback;
|
private @MonotonicNonNull BackgroundThreadStateHandler<Boolean> isSuitableForPlaybackState;
|
||||||
|
|
||||||
public ImplApi35(Context context, Handler eventHandler) {
|
|
||||||
this.applicationContext = context.getApplicationContext();
|
|
||||||
this.eventHandler = eventHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ThreadSafe") // Handler is thread-safe, but not annotated.
|
@SuppressLint("ThreadSafe") // Handler is thread-safe, but not annotated.
|
||||||
@Override
|
@Override
|
||||||
public void enable(Callback callback) {
|
public void enable(
|
||||||
router = MediaRouter2.getInstance(applicationContext);
|
Callback callback,
|
||||||
|
Context context,
|
||||||
|
Looper callbackLooper,
|
||||||
|
Looper backgroundLooper,
|
||||||
|
Clock clock) {
|
||||||
|
isSuitableForPlaybackState =
|
||||||
|
new BackgroundThreadStateHandler<>(
|
||||||
|
/* initialState= */ true,
|
||||||
|
backgroundLooper,
|
||||||
|
callbackLooper,
|
||||||
|
clock,
|
||||||
|
/* onStateChanged= */ (oldState, newState) ->
|
||||||
|
callback.onSelectedOutputSuitabilityChanged(newState));
|
||||||
|
isSuitableForPlaybackState.runInBackground(
|
||||||
|
() -> {
|
||||||
|
checkNotNull(isSuitableForPlaybackState);
|
||||||
|
router = MediaRouter2.getInstance(context);
|
||||||
routeCallback = new RouteCallback() {};
|
routeCallback = new RouteCallback() {};
|
||||||
Executor executor =
|
Executor backgroundExecutor = isSuitableForPlaybackState::runInBackground;
|
||||||
new Executor() {
|
router.registerRouteCallback(
|
||||||
@Override
|
backgroundExecutor, routeCallback, EMPTY_DISCOVERY_PREFERENCE);
|
||||||
public void execute(Runnable command) {
|
|
||||||
Util.postOrRun(eventHandler, command);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
router.registerRouteCallback(executor, routeCallback, EMPTY_DISCOVERY_PREFERENCE);
|
|
||||||
controllerCallback =
|
controllerCallback =
|
||||||
new ControllerCallback() {
|
new ControllerCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onControllerUpdated(RoutingController controller) {
|
public void onControllerUpdated(RoutingController controller) {
|
||||||
boolean isCurrentSelectedOutputSuitableForPlayback =
|
isSuitableForPlaybackState.setStateInBackground(
|
||||||
isSelectedOutputSuitableForPlayback(router);
|
isSelectedOutputSuitableForPlayback(router));
|
||||||
if (isSelectedOutputSuitableForPlayback
|
|
||||||
!= isCurrentSelectedOutputSuitableForPlayback) {
|
|
||||||
isSelectedOutputSuitableForPlayback = isCurrentSelectedOutputSuitableForPlayback;
|
|
||||||
callback.onSelectedOutputSuitabilityChanged(
|
|
||||||
isCurrentSelectedOutputSuitableForPlayback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
router.registerControllerCallback(executor, controllerCallback);
|
router.registerControllerCallback(backgroundExecutor, controllerCallback);
|
||||||
isSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback(router);
|
isSuitableForPlaybackState.setStateInBackground(
|
||||||
|
isSelectedOutputSuitableForPlayback(router));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disable() {
|
public void disable() {
|
||||||
checkStateNotNull(controllerCallback, "SuitableOutputChecker is not enabled");
|
checkStateNotNull(isSuitableForPlaybackState)
|
||||||
checkNotNull(router).unregisterControllerCallback(controllerCallback);
|
.runInBackground(
|
||||||
|
() -> {
|
||||||
|
checkNotNull(router).unregisterControllerCallback(checkNotNull(controllerCallback));
|
||||||
controllerCallback = null;
|
controllerCallback = null;
|
||||||
router.unregisterRouteCallback(checkNotNull(routeCallback));
|
router.unregisterRouteCallback(checkNotNull(routeCallback));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSelectedOutputSuitableForPlayback() {
|
public boolean isSelectedOutputSuitableForPlayback() {
|
||||||
return isSelectedOutputSuitableForPlayback;
|
return isSuitableForPlaybackState == null ? true : isSuitableForPlaybackState.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isSelectedOutputSuitableForPlayback(MediaRouter2 router) {
|
private static boolean isSelectedOutputSuitableForPlayback(MediaRouter2 router) {
|
||||||
@ -173,24 +178,34 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
@RequiresApi(23)
|
@RequiresApi(23)
|
||||||
private static final class ImplApi23 implements SuitableOutputChecker {
|
private static final class ImplApi23 implements SuitableOutputChecker {
|
||||||
|
|
||||||
private final Context applicationContext;
|
|
||||||
private final Handler eventHandler;
|
|
||||||
|
|
||||||
@Nullable private AudioManager audioManager;
|
@Nullable private AudioManager audioManager;
|
||||||
private @MonotonicNonNull AudioDeviceCallback audioDeviceCallback;
|
private @MonotonicNonNull AudioDeviceCallback audioDeviceCallback;
|
||||||
private boolean isSelectedOutputSuitableForPlayback;
|
private @MonotonicNonNull BackgroundThreadStateHandler<Boolean> isSuitableForPlaybackState;
|
||||||
|
|
||||||
public ImplApi23(Context context, Handler eventHandler) {
|
|
||||||
this.applicationContext = context.getApplicationContext();
|
|
||||||
this.eventHandler = eventHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enable(Callback callback) {
|
public void enable(
|
||||||
|
Callback callback,
|
||||||
|
Context context,
|
||||||
|
Looper callbackLooper,
|
||||||
|
Looper backgroundLooper,
|
||||||
|
Clock clock) {
|
||||||
|
isSuitableForPlaybackState =
|
||||||
|
new BackgroundThreadStateHandler<>(
|
||||||
|
/* initialState= */ true,
|
||||||
|
backgroundLooper,
|
||||||
|
callbackLooper,
|
||||||
|
clock,
|
||||||
|
/* onStateChanged= */ (oldState, newState) ->
|
||||||
|
callback.onSelectedOutputSuitabilityChanged(newState));
|
||||||
|
isSuitableForPlaybackState.runInBackground(
|
||||||
|
() -> {
|
||||||
|
checkNotNull(isSuitableForPlaybackState);
|
||||||
|
if (!Util.isWear(context)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
AudioManager audioManager =
|
AudioManager audioManager =
|
||||||
(AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
|
(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
if (audioManager == null) {
|
if (audioManager == null) {
|
||||||
isSelectedOutputSuitableForPlayback = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.audioManager = audioManager;
|
this.audioManager = audioManager;
|
||||||
@ -198,42 +213,37 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
new AudioDeviceCallback() {
|
new AudioDeviceCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
|
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
|
||||||
updateIsSelectedOutputSuitableForPlayback(callback);
|
isSuitableForPlaybackState.setStateInBackground(hasSupportedAudioOutput());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
|
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
|
||||||
updateIsSelectedOutputSuitableForPlayback(callback);
|
isSuitableForPlaybackState.setStateInBackground(hasSupportedAudioOutput());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
audioManager.registerAudioDeviceCallback(audioDeviceCallback, eventHandler);
|
audioManager.registerAudioDeviceCallback(
|
||||||
isSelectedOutputSuitableForPlayback = hasSupportedAudioOutput();
|
audioDeviceCallback, new Handler(checkNotNull(Looper.myLooper())));
|
||||||
|
isSuitableForPlaybackState.setStateInBackground(hasSupportedAudioOutput());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disable() {
|
public void disable() {
|
||||||
|
checkNotNull(isSuitableForPlaybackState)
|
||||||
|
.runInBackground(
|
||||||
|
() -> {
|
||||||
if (audioManager != null) {
|
if (audioManager != null) {
|
||||||
audioManager.unregisterAudioDeviceCallback(checkNotNull(audioDeviceCallback));
|
audioManager.unregisterAudioDeviceCallback(checkNotNull(audioDeviceCallback));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSelectedOutputSuitableForPlayback() {
|
public boolean isSelectedOutputSuitableForPlayback() {
|
||||||
return isSelectedOutputSuitableForPlayback;
|
return isSuitableForPlaybackState == null ? true : isSuitableForPlaybackState.get();
|
||||||
}
|
|
||||||
|
|
||||||
private void updateIsSelectedOutputSuitableForPlayback(Callback callback) {
|
|
||||||
boolean isSelectedOutputSuitableForPlayback = hasSupportedAudioOutput();
|
|
||||||
if (this.isSelectedOutputSuitableForPlayback != isSelectedOutputSuitableForPlayback) {
|
|
||||||
this.isSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback;
|
|
||||||
callback.onSelectedOutputSuitabilityChanged(isSelectedOutputSuitableForPlayback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasSupportedAudioOutput() {
|
private boolean hasSupportedAudioOutput() {
|
||||||
if (!Util.isWear(applicationContext)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
AudioDeviceInfo[] audioDeviceInfos =
|
AudioDeviceInfo[] audioDeviceInfos =
|
||||||
checkStateNotNull(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS);
|
checkStateNotNull(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS);
|
||||||
for (AudioDeviceInfo device : audioDeviceInfos) {
|
for (AudioDeviceInfo device : audioDeviceInfos) {
|
||||||
|
@ -24,7 +24,6 @@ import android.content.Context;
|
|||||||
import android.media.AudioDeviceInfo;
|
import android.media.AudioDeviceInfo;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
@ -460,7 +459,7 @@ public interface ExoPlayer extends Player {
|
|||||||
usePlatformDiagnostics = true;
|
usePlatformDiagnostics = true;
|
||||||
playerName = "";
|
playerName = "";
|
||||||
priority = C.PRIORITY_PLAYBACK;
|
priority = C.PRIORITY_PLAYBACK;
|
||||||
suitableOutputChecker = new DefaultSuitableOutputChecker(context, new Handler(looper));
|
suitableOutputChecker = new DefaultSuitableOutputChecker();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -413,7 +413,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||||||
|
|
||||||
if (builder.suppressPlaybackOnUnsuitableOutput) {
|
if (builder.suppressPlaybackOnUnsuitableOutput) {
|
||||||
suitableOutputChecker = builder.suitableOutputChecker;
|
suitableOutputChecker = builder.suitableOutputChecker;
|
||||||
suitableOutputChecker.enable(this::onSelectedOutputSuitabilityChanged);
|
suitableOutputChecker.enable(
|
||||||
|
this::onSelectedOutputSuitabilityChanged,
|
||||||
|
applicationContext,
|
||||||
|
applicationLooper,
|
||||||
|
playbackLooper,
|
||||||
|
clock);
|
||||||
} else {
|
} else {
|
||||||
suitableOutputChecker = null;
|
suitableOutputChecker = null;
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,10 @@ package androidx.media3.exoplayer;
|
|||||||
|
|
||||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Looper;
|
||||||
import androidx.annotation.RestrictTo;
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.media3.common.util.Clock;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
/** Provides methods to check the suitability of selected media outputs. */
|
/** Provides methods to check the suitability of selected media outputs. */
|
||||||
@ -45,8 +48,17 @@ public interface SuitableOutputChecker {
|
|||||||
* #disable()}.
|
* #disable()}.
|
||||||
*
|
*
|
||||||
* @param callback To receive notifications of changes in suitable media output changes.
|
* @param callback To receive notifications of changes in suitable media output changes.
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param callbackLooper The {@link Looper} to call {@link Callback} methods on.
|
||||||
|
* @param backgroundLooper The {@link Looper} to run background operations on.
|
||||||
|
* @param clock The {@link Clock}.
|
||||||
*/
|
*/
|
||||||
void enable(Callback callback);
|
void enable(
|
||||||
|
Callback callback,
|
||||||
|
Context context,
|
||||||
|
Looper callbackLooper,
|
||||||
|
Looper backgroundLooper,
|
||||||
|
Clock clock);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disables the current instance to receive updates on the selected media outputs and clears the
|
* Disables the current instance to receive updates on the selected media outputs and clears the
|
||||||
|
@ -15035,6 +15035,7 @@ public class ExoPlayerTest {
|
|||||||
parameterizeTestExoPlayerBuilder(
|
parameterizeTestExoPlayerBuilder(
|
||||||
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true))
|
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true))
|
||||||
.build();
|
.build();
|
||||||
|
advance(player).untilPendingCommandsAreFullyHandled();
|
||||||
player.setMediaItem(
|
player.setMediaItem(
|
||||||
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
||||||
player.addListener(
|
player.addListener(
|
||||||
@ -15051,7 +15052,7 @@ public class ExoPlayerTest {
|
|||||||
|
|
||||||
player.play();
|
player.play();
|
||||||
player.stop();
|
player.stop();
|
||||||
runUntilPlaybackState(player, Player.STATE_IDLE);
|
advance(player).untilState(Player.STATE_IDLE);
|
||||||
|
|
||||||
assertThat(playbackSuppressionList)
|
assertThat(playbackSuppressionList)
|
||||||
.containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
|
.containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
|
||||||
@ -15113,6 +15114,7 @@ public class ExoPlayerTest {
|
|||||||
parameterizeTestExoPlayerBuilder(
|
parameterizeTestExoPlayerBuilder(
|
||||||
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true))
|
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true))
|
||||||
.build();
|
.build();
|
||||||
|
advance(player).untilPendingCommandsAreFullyHandled();
|
||||||
player.setMediaItem(
|
player.setMediaItem(
|
||||||
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
||||||
player.addListener(
|
player.addListener(
|
||||||
@ -15131,7 +15133,7 @@ public class ExoPlayerTest {
|
|||||||
player.play();
|
player.play();
|
||||||
player.play();
|
player.play();
|
||||||
player.stop();
|
player.stop();
|
||||||
runUntilPlaybackState(player, Player.STATE_IDLE);
|
advance(player).untilState(Player.STATE_IDLE);
|
||||||
|
|
||||||
assertThat(playbackSuppressionList)
|
assertThat(playbackSuppressionList)
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
@ -15241,12 +15243,13 @@ public class ExoPlayerTest {
|
|||||||
player.prepare();
|
player.prepare();
|
||||||
player.play();
|
player.play();
|
||||||
player.pause();
|
player.pause();
|
||||||
runUntilPlaybackState(player, Player.STATE_READY);
|
advance(player).untilState(Player.STATE_READY);
|
||||||
|
|
||||||
addConnectedAudioOutput(
|
addConnectedAudioOutput(
|
||||||
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
|
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
|
||||||
|
advance(player).untilPendingCommandsAreFullyHandled();
|
||||||
player.stop();
|
player.stop();
|
||||||
runUntilPlaybackState(player, Player.STATE_IDLE);
|
advance(player).untilState(Player.STATE_IDLE);
|
||||||
|
|
||||||
assertThat(playbackSuppressionList)
|
assertThat(playbackSuppressionList)
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
@ -15298,11 +15301,12 @@ public class ExoPlayerTest {
|
|||||||
public void addSuitableOutputWhenPlaybackNotSuppressed_shouldNotRemovePlaybackSuppression()
|
public void addSuitableOutputWhenPlaybackNotSuppressed_shouldNotRemovePlaybackSuppression()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
addWatchAsSystemFeature();
|
addWatchAsSystemFeature();
|
||||||
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
|
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_USB_DEVICE);
|
||||||
ExoPlayer player =
|
ExoPlayer player =
|
||||||
parameterizeTestExoPlayerBuilder(
|
parameterizeTestExoPlayerBuilder(
|
||||||
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true))
|
new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true))
|
||||||
.build();
|
.build();
|
||||||
|
advance(player).untilPendingCommandsAreFullyHandled();
|
||||||
player.setMediaItem(
|
player.setMediaItem(
|
||||||
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
||||||
PlaybackSuppressionReasonChangedListener playbackSuppressionReasonChangedListener =
|
PlaybackSuppressionReasonChangedListener playbackSuppressionReasonChangedListener =
|
||||||
@ -15313,8 +15317,9 @@ public class ExoPlayerTest {
|
|||||||
|
|
||||||
addConnectedAudioOutput(
|
addConnectedAudioOutput(
|
||||||
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
|
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
|
||||||
|
advance(player).untilPendingCommandsAreFullyHandled();
|
||||||
player.stop();
|
player.stop();
|
||||||
runUntilPlaybackState(player, Player.STATE_IDLE);
|
advance(player).untilState(Player.STATE_IDLE);
|
||||||
|
|
||||||
assertThat(playbackSuppressionReasonChangedListener.getReasonChangedList()).isEmpty();
|
assertThat(playbackSuppressionReasonChangedListener.getReasonChangedList()).isEmpty();
|
||||||
player.release();
|
player.release();
|
||||||
@ -15341,14 +15346,15 @@ public class ExoPlayerTest {
|
|||||||
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
|
||||||
player.prepare();
|
player.prepare();
|
||||||
player.play();
|
player.play();
|
||||||
runUntilPlaybackState(player, Player.STATE_READY);
|
advance(player).untilState(Player.STATE_READY);
|
||||||
PlaybackSuppressionReasonChangedListener playbackSuppressionReasonChangedListener =
|
PlaybackSuppressionReasonChangedListener playbackSuppressionReasonChangedListener =
|
||||||
new PlaybackSuppressionReasonChangedListener();
|
new PlaybackSuppressionReasonChangedListener();
|
||||||
player.addListener(playbackSuppressionReasonChangedListener);
|
player.addListener(playbackSuppressionReasonChangedListener);
|
||||||
|
|
||||||
removeConnectedAudioOutput(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
|
removeConnectedAudioOutput(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
|
||||||
|
advance(player).untilPendingCommandsAreFullyHandled();
|
||||||
player.stop();
|
player.stop();
|
||||||
runUntilPlaybackState(player, Player.STATE_IDLE);
|
advance(player).untilState(Player.STATE_IDLE);
|
||||||
|
|
||||||
assertThat(playbackSuppressionReasonChangedListener.getReasonChangedList())
|
assertThat(playbackSuppressionReasonChangedListener.getReasonChangedList())
|
||||||
.containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
|
.containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
|
||||||
|
@ -18,8 +18,11 @@ package androidx.media3.test.utils;
|
|||||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
||||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Looper;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RestrictTo;
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.media3.common.util.Clock;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.exoplayer.SuitableOutputChecker;
|
import androidx.media3.exoplayer.SuitableOutputChecker;
|
||||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
@ -64,7 +67,12 @@ public final class FakeSuitableOutputChecker implements SuitableOutputChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enable(Callback callback) {
|
public void enable(
|
||||||
|
Callback callback,
|
||||||
|
Context context,
|
||||||
|
Looper callbackLooper,
|
||||||
|
Looper backgroundLooper,
|
||||||
|
Clock clock) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.ui;
|
package androidx.media3.ui;
|
||||||
|
|
||||||
|
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.advance;
|
||||||
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlayWhenReady;
|
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlayWhenReady;
|
||||||
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
|
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
|
||||||
import static androidx.test.ext.truth.content.IntentSubject.assertThat;
|
import static androidx.test.ext.truth.content.IntentSubject.assertThat;
|
||||||
@ -38,6 +39,7 @@ import androidx.media3.common.MediaItem;
|
|||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.Player.PlayWhenReadyChangeReason;
|
import androidx.media3.common.Player.PlayWhenReadyChangeReason;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
import androidx.media3.test.utils.FakeClock;
|
import androidx.media3.test.utils.FakeClock;
|
||||||
import androidx.media3.test.utils.FakeSuitableOutputChecker;
|
import androidx.media3.test.utils.FakeSuitableOutputChecker;
|
||||||
import androidx.media3.test.utils.TestExoPlayerBuilder;
|
import androidx.media3.test.utils.TestExoPlayerBuilder;
|
||||||
@ -75,11 +77,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
|
|||||||
|
|
||||||
private ShadowPackageManager shadowPackageManager;
|
private ShadowPackageManager shadowPackageManager;
|
||||||
private ShadowApplication shadowApplication;
|
private ShadowApplication shadowApplication;
|
||||||
private Player testPlayer;
|
private ExoPlayer testPlayer;
|
||||||
private FakeSuitableOutputChecker suitableMediaOutputChecker;
|
private FakeSuitableOutputChecker suitableMediaOutputChecker;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() throws Exception {
|
||||||
shadowPackageManager =
|
shadowPackageManager =
|
||||||
shadowOf(ApplicationProvider.getApplicationContext().getPackageManager());
|
shadowOf(ApplicationProvider.getApplicationContext().getPackageManager());
|
||||||
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
|
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
|
||||||
@ -95,6 +97,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
|
|||||||
builder.setSuitableOutputChecker(suitableMediaOutputChecker);
|
builder.setSuitableOutputChecker(suitableMediaOutputChecker);
|
||||||
}
|
}
|
||||||
testPlayer = builder.build();
|
testPlayer = builder.build();
|
||||||
|
advance(testPlayer).untilPendingCommandsAreFullyHandled();
|
||||||
|
|
||||||
shadowApplication = shadowOf((Application) ApplicationProvider.getApplicationContext());
|
shadowApplication = shadowOf((Application) ApplicationProvider.getApplicationContext());
|
||||||
}
|
}
|
||||||
@ -816,8 +819,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
|
|||||||
|
|
||||||
addConnectedAudioOutput(
|
addConnectedAudioOutput(
|
||||||
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
|
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
|
||||||
runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ false);
|
advance(testPlayer).untilPendingCommandsAreFullyHandled();
|
||||||
shadowOf(Looper.getMainLooper()).idle();
|
|
||||||
|
|
||||||
assertThat(ShadowPowerManager.getLatestWakeLock()).isNotNull();
|
assertThat(ShadowPowerManager.getLatestWakeLock()).isNotNull();
|
||||||
assertThat(ShadowPowerManager.getLatestWakeLock().isHeld()).isFalse();
|
assertThat(ShadowPowerManager.getLatestWakeLock().isHeld()).isFalse();
|
||||||
@ -959,7 +961,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
|
|||||||
fakeComponentName, new IntentFilter(fakeActionName));
|
fakeComponentName, new IntentFilter(fakeActionName));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupConnectedAudioOutput(int... deviceTypes) {
|
private void setupConnectedAudioOutput(int... deviceTypes) throws TimeoutException {
|
||||||
ShadowAudioManager shadowAudioManager =
|
ShadowAudioManager shadowAudioManager =
|
||||||
shadowOf(ApplicationProvider.getApplicationContext().getSystemService(AudioManager.class));
|
shadowOf(ApplicationProvider.getApplicationContext().getSystemService(AudioManager.class));
|
||||||
for (int deviceType : deviceTypes) {
|
for (int deviceType : deviceTypes) {
|
||||||
@ -967,6 +969,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
|
|||||||
AudioDeviceInfoBuilder.newBuilder().setType(deviceType).build(),
|
AudioDeviceInfoBuilder.newBuilder().setType(deviceType).build(),
|
||||||
/* notifyAudioDeviceCallbacks= */ true);
|
/* notifyAudioDeviceCallbacks= */ true);
|
||||||
}
|
}
|
||||||
|
advance(testPlayer).untilPendingCommandsAreFullyHandled();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addConnectedAudioOutput(int deviceTypes, boolean notifyAudioDeviceCallbacks) {
|
private void addConnectedAudioOutput(int deviceTypes, boolean notifyAudioDeviceCallbacks) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user