From 8bd1db5f2c41ced078576fb070528d14f4475632 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 14 Feb 2025 04:54:30 -0800 Subject: [PATCH] Move DefaultSuitableOutputChecker operations to playback thread PiperOrigin-RevId: 726879236 (cherry picked from commit e0ef6e51829dd1627d952d2677f532230cb2dbc9) --- .../DefaultSuitableOutputChecker.java | 202 +++++++++--------- .../androidx/media3/exoplayer/ExoPlayer.java | 3 +- .../media3/exoplayer/ExoPlayerImpl.java | 7 +- .../exoplayer/SuitableOutputChecker.java | 14 +- .../media3/exoplayer/ExoPlayerTest.java | 22 +- .../test/utils/FakeSuitableOutputChecker.java | 10 +- ...aybackSuppressionResolverListenerTest.java | 13 +- 7 files changed, 157 insertions(+), 114 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 c0655fb56d..fdf5ece407 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultSuitableOutputChecker.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultSuitableOutputChecker.java @@ -31,8 +31,11 @@ import android.media.MediaRouter2.RoutingController; import android.media.RouteDiscoveryPreference; import android.media.RoutingSessionInfo; import android.os.Handler; +import android.os.Looper; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.media3.common.util.BackgroundThreadStateHandler; +import androidx.media3.common.util.Clock; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; import java.util.concurrent.Executor; @@ -43,26 +46,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable private final SuitableOutputChecker impl; - /** - * Creates the default {@link SuitableOutputChecker}. - * - * @param context A {@link Context}. - * @param eventHandler A {@link Handler} to trigger {@link Callback} methods on. - */ - public DefaultSuitableOutputChecker(Context context, Handler eventHandler) { + /** Creates the default {@link SuitableOutputChecker}. */ + public DefaultSuitableOutputChecker() { if (Util.SDK_INT >= 35) { - impl = new ImplApi35(context, eventHandler); + impl = new ImplApi35(); } else if (Util.SDK_INT >= 23) { - impl = new ImplApi23(context, eventHandler); + impl = new ImplApi23(); } else { impl = null; } } @Override - public void enable(Callback callback) { + public void enable( + Callback callback, + Context context, + Looper callbackLooper, + Looper backgroundLooper, + Clock clock) { 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) .build(); - private final Context applicationContext; - private final Handler eventHandler; - private @MonotonicNonNull MediaRouter2 router; private @MonotonicNonNull RouteCallback routeCallback; @Nullable private ControllerCallback controllerCallback; - private boolean isSelectedOutputSuitableForPlayback; - - public ImplApi35(Context context, Handler eventHandler) { - this.applicationContext = context.getApplicationContext(); - this.eventHandler = eventHandler; - } + private @MonotonicNonNull BackgroundThreadStateHandler isSuitableForPlaybackState; @SuppressLint("ThreadSafe") // Handler is thread-safe, but not annotated. @Override - public void enable(Callback callback) { - router = MediaRouter2.getInstance(applicationContext); - routeCallback = new RouteCallback() {}; - Executor executor = - new Executor() { - @Override - public void execute(Runnable command) { - Util.postOrRun(eventHandler, command); - } - }; - router.registerRouteCallback(executor, routeCallback, EMPTY_DISCOVERY_PREFERENCE); - controllerCallback = - new ControllerCallback() { - @Override - public void onControllerUpdated(RoutingController controller) { - boolean isCurrentSelectedOutputSuitableForPlayback = - isSelectedOutputSuitableForPlayback(router); - if (isSelectedOutputSuitableForPlayback - != isCurrentSelectedOutputSuitableForPlayback) { - isSelectedOutputSuitableForPlayback = isCurrentSelectedOutputSuitableForPlayback; - callback.onSelectedOutputSuitabilityChanged( - isCurrentSelectedOutputSuitableForPlayback); - } - } - }; - router.registerControllerCallback(executor, controllerCallback); - isSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback(router); + 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); + router = MediaRouter2.getInstance(context); + routeCallback = new RouteCallback() {}; + Executor backgroundExecutor = isSuitableForPlaybackState::runInBackground; + router.registerRouteCallback( + backgroundExecutor, routeCallback, EMPTY_DISCOVERY_PREFERENCE); + controllerCallback = + new ControllerCallback() { + @Override + public void onControllerUpdated(RoutingController controller) { + isSuitableForPlaybackState.setStateInBackground( + isSelectedOutputSuitableForPlayback(router)); + } + }; + router.registerControllerCallback(backgroundExecutor, controllerCallback); + isSuitableForPlaybackState.setStateInBackground( + isSelectedOutputSuitableForPlayback(router)); + }); } @Override public void disable() { - checkStateNotNull(controllerCallback, "SuitableOutputChecker is not enabled"); - checkNotNull(router).unregisterControllerCallback(controllerCallback); - controllerCallback = null; - router.unregisterRouteCallback(checkNotNull(routeCallback)); + checkStateNotNull(isSuitableForPlaybackState) + .runInBackground( + () -> { + checkNotNull(router).unregisterControllerCallback(checkNotNull(controllerCallback)); + controllerCallback = null; + router.unregisterRouteCallback(checkNotNull(routeCallback)); + }); } @Override public boolean isSelectedOutputSuitableForPlayback() { - return isSelectedOutputSuitableForPlayback; + return isSuitableForPlaybackState == null ? true : isSuitableForPlaybackState.get(); } private static boolean isSelectedOutputSuitableForPlayback(MediaRouter2 router) { @@ -173,67 +178,72 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @RequiresApi(23) private static final class ImplApi23 implements SuitableOutputChecker { - private final Context applicationContext; - private final Handler eventHandler; - @Nullable private AudioManager audioManager; private @MonotonicNonNull AudioDeviceCallback audioDeviceCallback; - private boolean isSelectedOutputSuitableForPlayback; - - public ImplApi23(Context context, Handler eventHandler) { - this.applicationContext = context.getApplicationContext(); - this.eventHandler = eventHandler; - } + private @MonotonicNonNull BackgroundThreadStateHandler isSuitableForPlaybackState; @Override - public void enable(Callback callback) { - AudioManager audioManager = - (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE); - if (audioManager == null) { - isSelectedOutputSuitableForPlayback = true; - return; - } - this.audioManager = audioManager; - audioDeviceCallback = - new AudioDeviceCallback() { - @Override - public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { - updateIsSelectedOutputSuitableForPlayback(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) context.getSystemService(Context.AUDIO_SERVICE); + if (audioManager == null) { + return; + } + this.audioManager = audioManager; + audioDeviceCallback = + new AudioDeviceCallback() { + @Override + public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { + isSuitableForPlaybackState.setStateInBackground(hasSupportedAudioOutput()); + } - @Override - public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { - updateIsSelectedOutputSuitableForPlayback(callback); - } - }; - audioManager.registerAudioDeviceCallback(audioDeviceCallback, eventHandler); - isSelectedOutputSuitableForPlayback = hasSupportedAudioOutput(); + @Override + public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { + isSuitableForPlaybackState.setStateInBackground(hasSupportedAudioOutput()); + } + }; + audioManager.registerAudioDeviceCallback( + audioDeviceCallback, new Handler(checkNotNull(Looper.myLooper()))); + isSuitableForPlaybackState.setStateInBackground(hasSupportedAudioOutput()); + }); } @Override public void disable() { - if (audioManager != null) { - audioManager.unregisterAudioDeviceCallback(checkNotNull(audioDeviceCallback)); - } + checkNotNull(isSuitableForPlaybackState) + .runInBackground( + () -> { + if (audioManager != null) { + audioManager.unregisterAudioDeviceCallback(checkNotNull(audioDeviceCallback)); + } + }); } @Override public boolean isSelectedOutputSuitableForPlayback() { - return isSelectedOutputSuitableForPlayback; - } - - private void updateIsSelectedOutputSuitableForPlayback(Callback callback) { - boolean isSelectedOutputSuitableForPlayback = hasSupportedAudioOutput(); - if (this.isSelectedOutputSuitableForPlayback != isSelectedOutputSuitableForPlayback) { - this.isSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback; - callback.onSelectedOutputSuitabilityChanged(isSelectedOutputSuitableForPlayback); - } + return isSuitableForPlaybackState == null ? true : isSuitableForPlaybackState.get(); } private boolean hasSupportedAudioOutput() { - if (!Util.isWear(applicationContext)) { - return true; - } AudioDeviceInfo[] audioDeviceInfos = checkStateNotNull(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS); for (AudioDeviceInfo device : audioDeviceInfos) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index ce2b642216..51efcf6b58 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -24,7 +24,6 @@ import android.content.Context; import android.media.AudioDeviceInfo; import android.media.AudioTrack; import android.media.MediaCodec; -import android.os.Handler; import android.os.Looper; import android.os.Process; import android.view.Surface; @@ -460,7 +459,7 @@ public interface ExoPlayer extends Player { usePlatformDiagnostics = true; playerName = ""; priority = C.PRIORITY_PLAYBACK; - suitableOutputChecker = new DefaultSuitableOutputChecker(context, new Handler(looper)); + suitableOutputChecker = new DefaultSuitableOutputChecker(); } /** 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 280c70cd7a..52469d2ad3 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -413,7 +413,12 @@ import java.util.concurrent.CopyOnWriteArraySet; if (builder.suppressPlaybackOnUnsuitableOutput) { suitableOutputChecker = builder.suitableOutputChecker; - suitableOutputChecker.enable(this::onSelectedOutputSuitabilityChanged); + suitableOutputChecker.enable( + this::onSelectedOutputSuitabilityChanged, + applicationContext, + applicationLooper, + playbackLooper, + clock); } else { suitableOutputChecker = null; } 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 1bb8d9b3f5..cd744d21cd 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SuitableOutputChecker.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SuitableOutputChecker.java @@ -17,7 +17,10 @@ package androidx.media3.exoplayer; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; +import android.content.Context; +import android.os.Looper; import androidx.annotation.RestrictTo; +import androidx.media3.common.util.Clock; import androidx.media3.common.util.UnstableApi; /** Provides methods to check the suitability of selected media outputs. */ @@ -45,8 +48,17 @@ public interface SuitableOutputChecker { * #disable()}. * * @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 diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 2b77c04c93..a6f58e0bc1 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -15035,6 +15035,7 @@ public class ExoPlayerTest { parameterizeTestExoPlayerBuilder( new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true)) .build(); + advance(player).untilPendingCommandsAreFullyHandled(); player.setMediaItem( MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); player.addListener( @@ -15051,7 +15052,7 @@ public class ExoPlayerTest { player.play(); player.stop(); - runUntilPlaybackState(player, Player.STATE_IDLE); + advance(player).untilState(Player.STATE_IDLE); assertThat(playbackSuppressionList) .containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT); @@ -15113,6 +15114,7 @@ public class ExoPlayerTest { parameterizeTestExoPlayerBuilder( new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true)) .build(); + advance(player).untilPendingCommandsAreFullyHandled(); player.setMediaItem( MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); player.addListener( @@ -15131,7 +15133,7 @@ public class ExoPlayerTest { player.play(); player.play(); player.stop(); - runUntilPlaybackState(player, Player.STATE_IDLE); + advance(player).untilState(Player.STATE_IDLE); assertThat(playbackSuppressionList) .containsExactly( @@ -15241,12 +15243,13 @@ public class ExoPlayerTest { player.prepare(); player.play(); player.pause(); - runUntilPlaybackState(player, Player.STATE_READY); + advance(player).untilState(Player.STATE_READY); addConnectedAudioOutput( AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true); + advance(player).untilPendingCommandsAreFullyHandled(); player.stop(); - runUntilPlaybackState(player, Player.STATE_IDLE); + advance(player).untilState(Player.STATE_IDLE); assertThat(playbackSuppressionList) .containsExactly( @@ -15298,11 +15301,12 @@ public class ExoPlayerTest { public void addSuitableOutputWhenPlaybackNotSuppressed_shouldNotRemovePlaybackSuppression() throws Exception { addWatchAsSystemFeature(); - setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); + setupConnectedAudioOutput(AudioDeviceInfo.TYPE_USB_DEVICE); ExoPlayer player = parameterizeTestExoPlayerBuilder( new TestExoPlayerBuilder(context).setSuppressPlaybackOnUnsuitableOutput(true)) .build(); + advance(player).untilPendingCommandsAreFullyHandled(); player.setMediaItem( MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); PlaybackSuppressionReasonChangedListener playbackSuppressionReasonChangedListener = @@ -15313,8 +15317,9 @@ public class ExoPlayerTest { addConnectedAudioOutput( AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true); + advance(player).untilPendingCommandsAreFullyHandled(); player.stop(); - runUntilPlaybackState(player, Player.STATE_IDLE); + advance(player).untilState(Player.STATE_IDLE); assertThat(playbackSuppressionReasonChangedListener.getReasonChangedList()).isEmpty(); player.release(); @@ -15341,14 +15346,15 @@ public class ExoPlayerTest { MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); player.prepare(); player.play(); - runUntilPlaybackState(player, Player.STATE_READY); + advance(player).untilState(Player.STATE_READY); PlaybackSuppressionReasonChangedListener playbackSuppressionReasonChangedListener = new PlaybackSuppressionReasonChangedListener(); player.addListener(playbackSuppressionReasonChangedListener); removeConnectedAudioOutput(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP); + advance(player).untilPendingCommandsAreFullyHandled(); player.stop(); - runUntilPlaybackState(player, Player.STATE_IDLE); + advance(player).untilState(Player.STATE_IDLE); assertThat(playbackSuppressionReasonChangedListener.getReasonChangedList()) .containsExactly(Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT); 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 08279b4944..5238d4e122 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 @@ -18,8 +18,11 @@ package androidx.media3.test.utils; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static androidx.media3.common.util.Assertions.checkStateNotNull; +import android.content.Context; +import android.os.Looper; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; +import androidx.media3.common.util.Clock; import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.SuitableOutputChecker; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -64,7 +67,12 @@ public final class FakeSuitableOutputChecker implements SuitableOutputChecker { } @Override - public void enable(Callback callback) { + public void enable( + Callback callback, + Context context, + Looper callbackLooper, + Looper backgroundLooper, + Clock clock) { this.callback = callback; } diff --git a/libraries/ui/src/test/java/androidx/media3/ui/WearUnsuitableOutputPlaybackSuppressionResolverListenerTest.java b/libraries/ui/src/test/java/androidx/media3/ui/WearUnsuitableOutputPlaybackSuppressionResolverListenerTest.java index 7bd44c281b..52a621c733 100644 --- a/libraries/ui/src/test/java/androidx/media3/ui/WearUnsuitableOutputPlaybackSuppressionResolverListenerTest.java +++ b/libraries/ui/src/test/java/androidx/media3/ui/WearUnsuitableOutputPlaybackSuppressionResolverListenerTest.java @@ -15,6 +15,7 @@ */ 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.runUntilPlaybackState; 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.PlayWhenReadyChangeReason; import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.test.utils.FakeClock; import androidx.media3.test.utils.FakeSuitableOutputChecker; import androidx.media3.test.utils.TestExoPlayerBuilder; @@ -75,11 +77,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { private ShadowPackageManager shadowPackageManager; private ShadowApplication shadowApplication; - private Player testPlayer; + private ExoPlayer testPlayer; private FakeSuitableOutputChecker suitableMediaOutputChecker; @Before - public void setUp() { + public void setUp() throws Exception { shadowPackageManager = shadowOf(ApplicationProvider.getApplicationContext().getPackageManager()); shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); @@ -95,6 +97,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { builder.setSuitableOutputChecker(suitableMediaOutputChecker); } testPlayer = builder.build(); + advance(testPlayer).untilPendingCommandsAreFullyHandled(); shadowApplication = shadowOf((Application) ApplicationProvider.getApplicationContext()); } @@ -816,8 +819,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { addConnectedAudioOutput( AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true); - runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ false); - shadowOf(Looper.getMainLooper()).idle(); + advance(testPlayer).untilPendingCommandsAreFullyHandled(); assertThat(ShadowPowerManager.getLatestWakeLock()).isNotNull(); assertThat(ShadowPowerManager.getLatestWakeLock().isHeld()).isFalse(); @@ -959,7 +961,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { fakeComponentName, new IntentFilter(fakeActionName)); } - private void setupConnectedAudioOutput(int... deviceTypes) { + private void setupConnectedAudioOutput(int... deviceTypes) throws TimeoutException { ShadowAudioManager shadowAudioManager = shadowOf(ApplicationProvider.getApplicationContext().getSystemService(AudioManager.class)); for (int deviceType : deviceTypes) { @@ -967,6 +969,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { AudioDeviceInfoBuilder.newBuilder().setType(deviceType).build(), /* notifyAudioDeviceCallbacks= */ true); } + advance(testPlayer).untilPendingCommandsAreFullyHandled(); } private void addConnectedAudioOutput(int deviceTypes, boolean notifyAudioDeviceCallbacks) {