Pass down ControllerInfo from service to session when connecting

With this change, the `ControllerInfo` passed to
`MediaSessionService.onGetSession(ControllerInfo)`
is the same instance that is passed later to all
callback methods of `MediaSession.Callback`.

PiperOrigin-RevId: 568216855
This commit is contained in:
bachinger 2023-09-25 07:47:48 -07:00 committed by Copybara-Service
parent d965516dc9
commit 5e05e2ec22
10 changed files with 325 additions and 92 deletions

View File

@ -1030,21 +1030,8 @@ public class MediaSession {
/** Handles the controller's connection request from {@link MediaSessionService}. */ /** Handles the controller's connection request from {@link MediaSessionService}. */
/* package */ final void handleControllerConnectionFromService( /* package */ final void handleControllerConnectionFromService(
IMediaController controller, IMediaController controller, ControllerInfo controllerInfo) {
int controllerVersion, impl.connectFromService(controller, controllerInfo);
int controllerInterfaceVersion,
String packageName,
int pid,
int uid,
Bundle connectionHints) {
impl.connectFromService(
controller,
controllerVersion,
controllerInterfaceVersion,
packageName,
pid,
uid,
connectionHints);
} }
/* package */ final IBinder getLegacyBrowserServiceBinder() { /* package */ final IBinder getLegacyBrowserServiceBinder() {

View File

@ -641,22 +641,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
"Callback.onSetMediaItems must return a non-null future"); "Callback.onSetMediaItems must return a non-null future");
} }
public void connectFromService( public void connectFromService(IMediaController caller, ControllerInfo controllerInfo) {
IMediaController caller, sessionStub.connect(caller, controllerInfo);
int controllerVersion,
int controllerInterfaceVersion,
String packageName,
int pid,
int uid,
Bundle connectionHints) {
sessionStub.connect(
caller,
controllerVersion,
controllerInterfaceVersion,
packageName,
pid,
uid,
checkStateNotNull(connectionHints));
} }
public MediaSessionCompat getSessionCompat() { public MediaSessionCompat getSessionCompat() {

View File

@ -676,7 +676,7 @@ public abstract class MediaSessionService extends Service {
request.libraryVersion, request.libraryVersion,
request.controllerInterfaceVersion, request.controllerInterfaceVersion,
isTrusted, isTrusted,
/* cb= */ null, new MediaSessionStub.Controller2Cb(caller),
request.connectionHints); request.connectionHints);
@Nullable MediaSession session; @Nullable MediaSession session;
@ -689,14 +689,7 @@ public abstract class MediaSessionService extends Service {
service.addSession(session); service.addSession(session);
shouldNotifyDisconnected = false; shouldNotifyDisconnected = false;
session.handleControllerConnectionFromService( session.handleControllerConnectionFromService(caller, controllerInfo);
caller,
request.libraryVersion,
request.controllerInterfaceVersion,
request.packageName,
pid,
uid,
request.connectionHints);
} catch (Exception e) { } catch (Exception e) {
// Don't propagate exception in service to the controller. // Don't propagate exception in service to the controller.
Log.w(TAG, "Failed to add a session to session service", e); Log.w(TAG, "Failed to add a session to session service", e);

View File

@ -440,24 +440,8 @@ import java.util.concurrent.ExecutionException;
return mediaItemIndex; return mediaItemIndex;
} }
public void connect( @SuppressWarnings("UngroupedOverloads") // Overload belongs to AIDL interface that is separated.
IMediaController caller, public void connect(IMediaController caller, ControllerInfo controllerInfo) {
int controllerVersion,
int controllerInterfaceVersion,
String callingPackage,
int pid,
int uid,
Bundle connectionHints) {
MediaSessionManager.RemoteUserInfo remoteUserInfo =
new MediaSessionManager.RemoteUserInfo(callingPackage, pid, uid);
ControllerInfo controllerInfo =
new ControllerInfo(
remoteUserInfo,
controllerVersion,
controllerInterfaceVersion,
sessionManager.isTrustedForMediaControl(remoteUserInfo),
new Controller2Cb(caller),
connectionHints);
@Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get(); @Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
if (sessionImpl == null || sessionImpl.isReleased()) { if (sessionImpl == null || sessionImpl.isReleased()) {
try { try {
@ -612,14 +596,18 @@ import java.util.concurrent.ExecutionException;
// If it's the case, use PID from the ConnectionRequest. // If it's the case, use PID from the ConnectionRequest.
int pid = (callingPid != 0) ? callingPid : request.pid; int pid = (callingPid != 0) ? callingPid : request.pid;
try { try {
connect(
caller, MediaSessionManager.RemoteUserInfo remoteUserInfo =
new MediaSessionManager.RemoteUserInfo(request.packageName, pid, uid);
ControllerInfo controllerInfo =
new ControllerInfo(
remoteUserInfo,
request.libraryVersion, request.libraryVersion,
request.controllerInterfaceVersion, request.controllerInterfaceVersion,
request.packageName, sessionManager.isTrustedForMediaControl(remoteUserInfo),
pid, new MediaSessionStub.Controller2Cb(caller),
uid,
request.connectionHints); request.connectionHints);
connect(caller, controllerInfo);
} finally { } finally {
Binder.restoreCallingIdentity(token); Binder.restoreCallingIdentity(token);
} }

View File

@ -0,0 +1,117 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.session;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
import android.content.ComponentName;
import android.content.Context;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.session.MediaLibraryService.MediaLibrarySession;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link MediaLibraryService}. */
@RunWith(AndroidJUnit4.class)
@MediumTest
public class MediaLibraryServiceTest {
@Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule();
private Context context;
private SessionToken token;
@Before
public void setUp() {
TestServiceRegistry.getInstance().cleanUp();
context = ApplicationProvider.getApplicationContext();
token =
new SessionToken(context, new ComponentName(context, LocalMockMediaLibraryService.class));
}
@After
public void cleanUp() {
TestServiceRegistry.getInstance().cleanUp();
}
@Test
public void onConnect_controllerInfo_sameInstanceInOnGetSessionAndCallback() throws Exception {
TestServiceRegistry testServiceRegistry = TestServiceRegistry.getInstance();
List<MediaSession.ControllerInfo> onGetSessionControllerInfos = new ArrayList<>();
List<MediaSession.ControllerInfo> browserCommandControllerInfos = new ArrayList<>();
AtomicReference<MediaSession> session = new AtomicReference<>();
testServiceRegistry.setOnGetSessionHandler(
controllerInfo -> {
MockMediaLibraryService service =
(MockMediaLibraryService) testServiceRegistry.getServiceInstance();
// The controllerInfo passed to the onGetSession of the service.
onGetSessionControllerInfos.add(controllerInfo);
Player player = new ExoPlayer.Builder(context).build();
MediaLibrarySession.Callback callback =
new MediaLibrarySession.Callback() {
@Override
public ListenableFuture<LibraryResult<MediaItem>> onGetItem(
MediaLibrarySession session,
MediaSession.ControllerInfo browser,
String mediaId) {
browserCommandControllerInfos.add(browser);
return Futures.immediateFuture(
LibraryResult.ofItem(
new MediaItem.Builder()
.setMediaId("media-id-321")
.setMediaMetadata(
new MediaMetadata.Builder()
.setIsPlayable(false)
.setIsBrowsable(true)
.build())
.build(),
/* params= */ null));
}
};
session.set(new MediaLibrarySession.Builder(service, player, callback).build());
return session.get();
});
// Create the remote browser to start the service.
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(token);
// Get the started service instance after creation.
MockMediaLibraryService service =
(MockMediaLibraryService) testServiceRegistry.getServiceInstance();
assertThat(browser.getItem("mediaId").resultCode).isEqualTo(LibraryResult.RESULT_SUCCESS);
browser.release();
service.blockUntilAllControllersUnbind(TIMEOUT_MS);
assertThat(onGetSessionControllerInfos).hasSize(1);
assertThat(browserCommandControllerInfos).hasSize(1);
assertThat(onGetSessionControllerInfos.get(0))
.isSameInstanceAs(browserCommandControllerInfos.get(0));
}
}

View File

@ -29,6 +29,7 @@ import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.media.session.PlaybackStateCompat;
import androidx.media3.common.ForwardingPlayer;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.ConditionVariable;
@ -52,6 +53,7 @@ import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
@ -66,23 +68,20 @@ public class MediaSessionServiceTest {
@ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule();
@Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule();
@Rule public final MediaSessionTestRule sessionTestRule = new MediaSessionTestRule();
@Rule @Rule
public final HandlerThreadTestRule threadTestRule = public final HandlerThreadTestRule threadTestRule =
new HandlerThreadTestRule("MediaSessionServiceTest"); new HandlerThreadTestRule("MediaSessionServiceTest");
@Rule public final RemoteControllerTestRule controllerTestRule = new RemoteControllerTestRule();
@Rule public final MediaSessionTestRule sessionTestRule = new MediaSessionTestRule();
private Context context; private Context context;
private Looper looper;
private SessionToken token; private SessionToken token;
@Before @Before
public void setUp() { public void setUp() {
TestServiceRegistry.getInstance().cleanUp(); TestServiceRegistry.getInstance().cleanUp();
context = ApplicationProvider.getApplicationContext(); context = ApplicationProvider.getApplicationContext();
looper = threadTestRule.getHandler().getLooper();
token = token =
new SessionToken(context, new ComponentName(context, LocalMockMediaSessionService.class)); new SessionToken(context, new ComponentName(context, LocalMockMediaSessionService.class));
} }
@ -92,6 +91,91 @@ public class MediaSessionServiceTest {
TestServiceRegistry.getInstance().cleanUp(); TestServiceRegistry.getInstance().cleanUp();
} }
@Test
public void onConnect_controllerInfo_sameInstanceFromServiceToConnectedControllerManager()
throws Exception {
TestServiceRegistry testServiceRegistry = TestServiceRegistry.getInstance();
List<ControllerInfo> onGetSessionControllerInfos = new ArrayList<>();
List<ControllerInfo> onConnectControllerInfos = new ArrayList<>();
List<ControllerInfo> playbackCommandControllerInfos = new ArrayList<>();
List<ControllerInfo> onDisconnectedCommandControllerInfos = new ArrayList<>();
AtomicReference<MediaSession> session = new AtomicReference<>();
testServiceRegistry.setOnGetSessionHandler(
controllerInfo -> {
// The controllerInfo passed to the onGetSession of the service.
onGetSessionControllerInfos.add(controllerInfo);
Player player = new ExoPlayer.Builder(context).build();
ForwardingPlayer forwardingPlayer =
new ForwardingPlayer(player) {
@Override
public void setRepeatMode(@Player.RepeatMode int repeatMode) {
// The controllerInfo assigned for the current player command request.
playbackCommandControllerInfos.add(
session.get().getControllerForCurrentRequest());
super.setRepeatMode(repeatMode);
}
};
session.set(
new MediaSession.Builder(context, forwardingPlayer)
.setCallback(
new MediaSession.Callback() {
@Override
public MediaSession.ConnectionResult onConnect(
MediaSession session, ControllerInfo controller) {
if (!session.isMediaNotificationController(controller)) {
// The controllerInfo passed to MediaSession.Callback.onConnect()
onConnectControllerInfos.add(controllerInfo);
}
return MediaSession.Callback.super.onConnect(session, controller);
}
@Override
public void onDisconnected(
MediaSession session, ControllerInfo controller) {
if (!session.isMediaNotificationController(controller)) {
// The controllerInfo when disconnecting.
onDisconnectedCommandControllerInfos.add(controller);
}
MediaSession.Callback.super.onDisconnected(session, controller);
}
})
.build());
return session.get();
});
// Create the remote controller to start the service.
RemoteMediaController controller =
controllerTestRule.createRemoteController(
token, /* waitForConnection= */ true, /* connectionHints= */ null);
// Get the started service instance after creation.
MockMediaSessionService service =
(MockMediaSessionService) testServiceRegistry.getServiceInstance();
controller.setRepeatMode(Player.REPEAT_MODE_ONE);
List<ControllerInfo> connectedControllerManagerControllerInfos = new ArrayList<>();
for (ControllerInfo controllerInfo : session.get().getConnectedControllers()) {
if (!session.get().isMediaNotificationController(controllerInfo)) {
// The controllerInfo in the connected controller manager.
connectedControllerManagerControllerInfos.add(controllerInfo);
}
}
controller.release();
service.blockUntilAllControllersUnbind(TIMEOUT_MS);
assertThat(onGetSessionControllerInfos).hasSize(1);
assertThat(onGetSessionControllerInfos).isEqualTo(onConnectControllerInfos);
assertThat(onGetSessionControllerInfos).isEqualTo(playbackCommandControllerInfos);
assertThat(onGetSessionControllerInfos).isEqualTo(onDisconnectedCommandControllerInfos);
assertThat(onGetSessionControllerInfos).isEqualTo(connectedControllerManagerControllerInfos);
assertThat(onGetSessionControllerInfos.get(0))
.isSameInstanceAs(onConnectControllerInfos.get(0));
assertThat(onGetSessionControllerInfos.get(0))
.isSameInstanceAs(playbackCommandControllerInfos.get(0));
assertThat(onGetSessionControllerInfos.get(0))
.isSameInstanceAs(onDisconnectedCommandControllerInfos.get(0));
assertThat(onGetSessionControllerInfos.get(0))
.isSameInstanceAs(connectedControllerManagerControllerInfos.get(0));
}
@Test @Test
public void controllerRelease_keepsControllerBoundUntilCommandsHandled() throws Exception { public void controllerRelease_keepsControllerBoundUntilCommandsHandled() throws Exception {
TestServiceRegistry testServiceRegistry = TestServiceRegistry.getInstance(); TestServiceRegistry testServiceRegistry = TestServiceRegistry.getInstance();
@ -291,7 +375,10 @@ public class MediaSessionServiceTest {
MediaSession testSession = MediaSession testSession =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder( new MediaSession.Builder(
context, new MockPlayer.Builder().setApplicationLooper(looper).build()) context,
new MockPlayer.Builder()
.setApplicationLooper(threadTestRule.getHandler().getLooper())
.build())
.setId("testOnGetSession_returnsSession") .setId("testOnGetSession_returnsSession")
.setCallback( .setCallback(
new MediaSession.Callback() { new MediaSession.Callback() {
@ -370,7 +457,9 @@ public class MediaSessionServiceTest {
public void onGetSession_rejectsConnection() throws Exception { public void onGetSession_rejectsConnection() throws Exception {
TestServiceRegistry.getInstance().setOnGetSessionHandler(controllerInfo -> null); TestServiceRegistry.getInstance().setOnGetSessionHandler(controllerInfo -> null);
ListenableFuture<MediaController> future = ListenableFuture<MediaController> future =
new MediaController.Builder(context, token).setApplicationLooper(looper).buildAsync(); new MediaController.Builder(context, token)
.setApplicationLooper(threadTestRule.getHandler().getLooper())
.buildAsync();
ExecutionException thrown = ExecutionException thrown =
assertThrows(ExecutionException.class, () -> future.get(TIMEOUT_MS, MILLISECONDS)); assertThrows(ExecutionException.class, () -> future.get(TIMEOUT_MS, MILLISECONDS));
@ -481,7 +570,10 @@ public class MediaSessionServiceTest {
private MediaSession createMediaSession(String id) { private MediaSession createMediaSession(String id) {
return sessionTestRule.ensureReleaseAfterTest( return sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder( new MediaSession.Builder(
context, new MockPlayer.Builder().setApplicationLooper(looper).build()) context,
new MockPlayer.Builder()
.setApplicationLooper(threadTestRule.getHandler().getLooper())
.build())
.setId(id) .setId(id)
.build()); .build());
} }

View File

@ -106,6 +106,14 @@
</intent-filter> </intent-filter>
</service> </service>
<service android:name="androidx.media3.session.LocalMockMediaLibraryService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService" />
</intent-filter>
</service>
<service android:name="androidx.media3.session.MockMediaBrowserServiceCompat" <service android:name="androidx.media3.session.MockMediaBrowserServiceCompat"
android:exported="true" android:exported="true"
android:process=":remote"> android:process=":remote">

View File

@ -0,0 +1,19 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.session;
/** {@link MockMediaLibraryService} running on a local process. */
public class LocalMockMediaLibraryService extends MockMediaLibraryService {}

View File

@ -60,15 +60,16 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.IBinder;
import androidx.annotation.GuardedBy; import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.test.session.common.CommonConstants; import androidx.media3.test.session.common.CommonConstants;
import androidx.media3.test.session.common.TestHandler;
import androidx.media3.test.session.common.TestUtils; import androidx.media3.test.session.common.TestUtils;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
@ -77,6 +78,8 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
/** A mock MediaLibraryService */ /** A mock MediaLibraryService */
public class MockMediaLibraryService extends MediaLibraryService { public class MockMediaLibraryService extends MediaLibraryService {
@ -119,9 +122,36 @@ public class MockMediaLibraryService extends MediaLibraryService {
@Nullable private static byte[] testArtworkData; @Nullable private static byte[] testArtworkData;
MediaLibrarySession session; private final AtomicInteger boundControllerCount;
TestHandler handler; private final ConditionVariable allControllersUnbound;
HandlerThread handlerThread;
@Nullable MediaLibrarySession session;
@Nullable HandlerThread handlerThread;
public MockMediaLibraryService() {
boundControllerCount = new AtomicInteger(/* initialValue= */ 0);
allControllersUnbound = new ConditionVariable();
allControllersUnbound.open();
}
/** Returns whether at least one controller is bound to this service. */
public boolean hasBoundController() {
return !allControllersUnbound.isOpen();
}
/**
* Blocks until all bound controllers unbind.
*
* @param timeoutMs The block timeout in milliseconds.
* @throws TimeoutException If the block timed out.
* @throws InterruptedException If the block was interrupted.
*/
public void blockUntilAllControllersUnbind(long timeoutMs)
throws TimeoutException, InterruptedException {
if (!allControllersUnbound.block(timeoutMs)) {
throw new TimeoutException();
}
}
@Override @Override
public void onCreate() { public void onCreate() {
@ -129,7 +159,21 @@ public class MockMediaLibraryService extends MediaLibraryService {
super.onCreate(); super.onCreate();
handlerThread = new HandlerThread(TAG); handlerThread = new HandlerThread(TAG);
handlerThread.start(); handlerThread.start();
handler = new TestHandler(handlerThread.getLooper()); }
@Override
public IBinder onBind(@Nullable Intent intent) {
boundControllerCount.incrementAndGet();
allControllersUnbound.close();
return super.onBind(intent);
}
@Override
public boolean onUnbind(Intent intent) {
if (boundControllerCount.decrementAndGet() == 0) {
allControllersUnbound.open();
}
return super.onUnbind(intent);
} }
@Override @Override
@ -141,9 +185,9 @@ public class MockMediaLibraryService extends MediaLibraryService {
} }
TestServiceRegistry.getInstance().cleanUp(); TestServiceRegistry.getInstance().cleanUp();
if (Util.SDK_INT >= 18) { if (Util.SDK_INT >= 18) {
handler.getLooper().quitSafely(); handlerThread.quitSafely();
} else { } else {
handler.getLooper().quit(); handlerThread.quit();
} }
} }
@ -157,7 +201,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
if (session == null) { if (session == null) {
MockPlayer player = MockPlayer player =
new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); new MockPlayer.Builder().setApplicationLooper(handlerThread.getLooper()).build();
MediaLibrarySession.Callback callback = registry.getSessionCallback(); MediaLibrarySession.Callback callback = registry.getSessionCallback();
session = session =

View File

@ -34,11 +34,10 @@ public class MockMediaSessionService extends MediaSessionService {
public static final String ID = "TestSession"; public static final String ID = "TestSession";
private final AtomicInteger boundControllerCount; private final AtomicInteger boundControllerCount;
private final ConditionVariable allControllersUnbound;
public MediaSession session; @Nullable public MediaSession session;
@Nullable private HandlerThread handlerThread;
private HandlerThread handlerThread;
private ConditionVariable allControllersUnbound;
public MockMediaSessionService() { public MockMediaSessionService() {
boundControllerCount = new AtomicInteger(/* initialValue= */ 0); boundControllerCount = new AtomicInteger(/* initialValue= */ 0);