MediaRouter2: Add (un)registerSessionCallback

This CL pulls out the process of registering SessionCallback
from the method MediaRouter2#requestCreateSession.

Now the registered callback can be reused until unregistered.

Also, this renames existing (un)registerCallback methods to
(un)register'Route'Callback.

Bug: 146400872
Test: atest mediaroutertest
Change-Id: I68f71b0a857780ab9e2a0ca2a550af5ab4962f70
This commit is contained in:
Hyundo Moon
2019-12-27 17:41:17 +09:00
parent 7b36ffb06c
commit a64ca9ec9e
4 changed files with 292 additions and 77 deletions

View File

@@ -103,6 +103,9 @@ public class MediaRouter2 {
private final CopyOnWriteArrayList<RouteCallbackRecord> mRouteCallbackRecords =
new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<SessionCallbackRecord> mSessionCallbackRecords =
new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<SessionCreationRequest> mSessionCreationRequests =
new CopyOnWriteArrayList<>();
@@ -164,9 +167,9 @@ public class MediaRouter2 {
/**
* Registers a callback to discover routes and to receive events when they change.
*/
public void registerCallback(@NonNull @CallbackExecutor Executor executor,
public void registerRouteCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull RouteCallback routeCallback) {
registerCallback(executor, routeCallback, 0);
registerRouteCallback(executor, routeCallback, 0);
}
/**
@@ -175,7 +178,7 @@ public class MediaRouter2 {
* If you register the same callback twice or more, it will be ignored.
* </p>
*/
public void registerCallback(@NonNull @CallbackExecutor Executor executor,
public void registerRouteCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull RouteCallback routeCallback, int flags) {
Objects.requireNonNull(executor, "executor must not be null");
Objects.requireNonNull(routeCallback, "callback must not be null");
@@ -207,9 +210,9 @@ public class MediaRouter2 {
* If the callback has not been added or been removed already, it is ignored.
*
* @param routeCallback the callback to unregister
* @see #registerCallback
* @see #registerRouteCallback
*/
public void unregisterCallback(@NonNull RouteCallback routeCallback) {
public void unregisterRouteCallback(@NonNull RouteCallback routeCallback) {
Objects.requireNonNull(routeCallback, "callback must not be null");
if (!mRouteCallbackRecords.remove(
@@ -283,30 +286,60 @@ public class MediaRouter2 {
return mFilteredRoutes;
}
/**
* Registers a callback to get updates on creations and changes of route sessions.
* If you register the same callback twice or more, it will be ignored.
*
* @param executor the executor to execute the callback on
* @param callback the callback to register
* @see #unregisterSessionCallback
*/
@NonNull
public void registerSessionCallback(@CallbackExecutor Executor executor,
@NonNull SessionCallback callback) {
Objects.requireNonNull(executor, "executor must not be null");
Objects.requireNonNull(callback, "callback must not be null");
SessionCallbackRecord record = new SessionCallbackRecord(executor, callback);
if (!mSessionCallbackRecords.addIfAbsent(record)) {
Log.w(TAG, "Ignoring the same session callback");
return;
}
}
/**
* Unregisters the given callback. The callback will no longer receive events.
* If the callback has not been added or been removed already, it is ignored.
*
* @param callback the callback to unregister
* @see #registerSessionCallback
*/
@NonNull
public void unregisterSessionCallback(@NonNull SessionCallback callback) {
Objects.requireNonNull(callback, "callback must not be null");
if (!mSessionCallbackRecords.remove(new SessionCallbackRecord(null, callback))) {
Log.w(TAG, "Ignoring unknown session callback");
return;
}
}
/**
* Requests the media route provider service to create a session with the given route.
*
* @param route the route you want to create a session with.
* @param controlCategory the control category of the session. Should not be empty
* @param executor the executor to get the result of the session creation
* @param callback the callback to get the result of the session creation
*
* @see SessionCallback#onSessionCreated(RouteSessionController, Bundle)
* @see SessionCallback#onSessionCreationFailed()
*
* TODO: Separate callback registeration from creating session request.
* @see SessionCallback#onSessionCreated
* @see SessionCallback#onSessionCreationFailed
*/
@NonNull
public void requestCreateSession(@NonNull MediaRoute2Info route,
@NonNull String controlCategory,
@CallbackExecutor Executor executor, @NonNull SessionCallback callback) {
@NonNull String controlCategory) {
Objects.requireNonNull(route, "route must not be null");
if (TextUtils.isEmpty(controlCategory)) {
throw new IllegalArgumentException("controlCategory must not be empty");
}
Objects.requireNonNull(executor, "executor must not be null");
Objects.requireNonNull(callback, "callback must not be null");
// TODO: Check the given route exists
// TODO: Check the route supports the given controlCategory
@@ -316,7 +349,7 @@ public class MediaRouter2 {
requestId = Process.myPid() * 10000 + mSessionCreationRequestCnt.getAndIncrement();
SessionCreationRequest request = new SessionCreationRequest(
requestId, route, controlCategory, executor, callback);
requestId, route, controlCategory);
mSessionCreationRequests.add(request);
Client2 client;
@@ -492,9 +525,11 @@ public class MediaRouter2 {
/**
* Creates a controller and calls the {@link SessionCallback#onSessionCreated}.
* If session creation has failed, then it calls
* {@link SessionCallback#onSessionCreationFailed()}.
* {@link SessionCallback#onSessionCreationFailed}.
* <p>
* Pass {@code null} to sessionInfo for the failure case.
*
* TODO: What should router do when the session is created by manager?
*/
void createControllerOnHandler(@Nullable RouteSessionInfo sessionInfo, int requestId) {
SessionCreationRequest matchingRequest = null;
@@ -513,18 +548,29 @@ public class MediaRouter2 {
mSessionCreationRequests.remove(matchingRequest);
final Executor executor = matchingRequest.mExecutor;
final SessionCallback callback = matchingRequest.mSessionCallback;
MediaRoute2Info requestedRoute = matchingRequest.mRoute;
String requestedControlCategory = matchingRequest.mControlCategory;
// TODO: Also check provider ID when RouteSessionInfo#getProviderId() is introduced.
if (sessionInfo == null) {
// TODO: We may need to distinguish between failure and rejection.
// One way can be introducing 'reason'.
executor.execute(callback::onSessionCreationFailed);
notifySessionCreationFailed(requestedRoute, requestedControlCategory);
} else if (!TextUtils.equals(requestedControlCategory, sessionInfo.getControlCategory())) {
Log.w(TAG, "The session has different control category from what we requested. "
+ "(requested=" + requestedControlCategory
+ ", actual=" + sessionInfo.getControlCategory()
+ ")");
notifySessionCreationFailed(requestedRoute, requestedControlCategory);
} else if (!sessionInfo.getSelectedRoutes().contains(requestedRoute.getId())) {
Log.w(TAG, "The session does not contain the requested route. "
+ "(requestedRouteId=" + requestedRoute.getId()
+ ", actualRoutes=" + sessionInfo.getSelectedRoutes()
+ ")");
notifySessionCreationFailed(requestedRoute, requestedControlCategory);
} else {
// TODO: RouteSessionController should be created with full info (e.g. routes)
// from RouteSessionInfo.
RouteSessionController controller = new RouteSessionController(sessionInfo);
executor.execute(() -> callback.onSessionCreated(controller));
notifySessionCreated(controller);
}
}
@@ -549,6 +595,20 @@ public class MediaRouter2 {
}
}
private void notifySessionCreated(RouteSessionController controller) {
for (SessionCallbackRecord record: mSessionCallbackRecords) {
record.mExecutor.execute(
() -> record.mSessionCallback.onSessionCreated(controller));
}
}
private void notifySessionCreationFailed(MediaRoute2Info route, String controlCategory) {
for (SessionCallbackRecord record: mSessionCallbackRecords) {
record.mExecutor.execute(
() -> record.mSessionCallback.onSessionCreationFailed(route, controlCategory));
}
}
/**
* Callback for receiving events about media route discovery.
*/
@@ -594,8 +654,12 @@ public class MediaRouter2 {
/**
* Called when the session creation request failed.
*
* @param requestedRoute the route info which was used for the request
* @param requestedControlCategory the control category which was used for the request
*/
public void onSessionCreationFailed() {}
public void onSessionCreationFailed(MediaRoute2Info requestedRoute,
String requestedControlCategory) {}
/**
* Called when the session info has changed.
@@ -787,21 +851,42 @@ public class MediaRouter2 {
}
}
final class SessionCallbackRecord {
public final Executor mExecutor;
public final SessionCallback mSessionCallback;
SessionCallbackRecord(@NonNull Executor executor,
@NonNull SessionCallback sessionCallback) {
mSessionCallback = sessionCallback;
mExecutor = executor;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SessionCallbackRecord)) {
return false;
}
return mSessionCallback == ((SessionCallbackRecord) obj).mSessionCallback;
}
@Override
public int hashCode() {
return mSessionCallback.hashCode();
}
}
final class SessionCreationRequest {
public final MediaRoute2Info mRoute;
public final String mControlCategory;
public final Executor mExecutor;
public final SessionCallback mSessionCallback;
public final int mRequestId;
SessionCreationRequest(int requestId, @NonNull MediaRoute2Info route,
@NonNull String controlCategory,
@Nullable Executor executor,
@NonNull SessionCallback sessionCallback) {
@NonNull String controlCategory) {
mRoute = route;
mControlCategory = controlCategory;
mExecutor = executor;
mSessionCallback = sessionCallback;
mRequestId = requestId;
}
}

View File

@@ -58,10 +58,9 @@ public class SampleMediaRoute2ProviderService extends MediaRoute2ProviderService
public static final String CATEGORY_SPECIAL =
"com.android.mediarouteprovider.CATEGORY_SPECIAL";
public static final int SESSION_ID_1 = 1000;
Map<String, MediaRoute2Info> mRoutes = new HashMap<>();
Map<String, Integer> mRouteSessionMap = new HashMap<>();
private int mNextSessionId = 1000;
private void initializeRoutes() {
MediaRoute2Info route1 = new MediaRoute2Info.Builder(ROUTE_ID1, ROUTE_NAME1)
@@ -163,7 +162,8 @@ public class SampleMediaRoute2ProviderService extends MediaRoute2ProviderService
}
maybeRemoveRoute(routeId);
int sessionId = SESSION_ID_1;
final int sessionId = mNextSessionId;
mNextSessionId++;
mRoutes.put(routeId, new MediaRoute2Info.Builder(route)
.setClientPackageName(packageName)

View File

@@ -28,6 +28,7 @@ import static com.android.mediaroutertest.MediaRouterManagerTest.CATEGORIES_SPEC
import static com.android.mediaroutertest.MediaRouterManagerTest.CATEGORY_SAMPLE;
import static com.android.mediaroutertest.MediaRouterManagerTest.CATEGORY_SPECIAL;
import static com.android.mediaroutertest.MediaRouterManagerTest.ROUTE_ID1;
import static com.android.mediaroutertest.MediaRouterManagerTest.ROUTE_ID2;
import static com.android.mediaroutertest.MediaRouterManagerTest.ROUTE_ID3_SESSION_CREATION_FAILED;
import static com.android.mediaroutertest.MediaRouterManagerTest.ROUTE_ID_SPECIAL_CATEGORY;
import static com.android.mediaroutertest.MediaRouterManagerTest.ROUTE_ID_VARIABLE_VOLUME;
@@ -43,6 +44,7 @@ import static org.testng.Assert.assertThrows;
import android.content.Context;
import android.media.MediaRoute2Info;
import android.media.MediaRouter2;
import android.media.MediaRouter2.RouteCallback;
import android.media.MediaRouter2.RouteSessionController;
import android.media.MediaRouter2.SessionCallback;
import android.net.Uri;
@@ -205,29 +207,40 @@ public class MediaRouter2Test {
}
@Test
public void testRequestCreateSessionWithInvalidArguments() {
MediaRoute2Info route = new MediaRoute2Info.Builder("id", "name").build();
String controlCategory = "controlCategory";
public void testRegisterSessionCallbackWithInvalidArguments() {
Executor executor = mExecutor;
SessionCallback callback = new SessionCallback();
// Tests null route
assertThrows(NullPointerException.class,
() -> mRouter2.requestCreateSession(null, controlCategory, executor, callback));
// Tests null or empty control category
assertThrows(IllegalArgumentException.class,
() -> mRouter2.requestCreateSession(route, null, executor, callback));
assertThrows(IllegalArgumentException.class,
() -> mRouter2.requestCreateSession(route, "", executor, callback));
// Tests null executor
assertThrows(NullPointerException.class,
() -> mRouter2.requestCreateSession(route, controlCategory, null, callback));
() -> mRouter2.registerSessionCallback(null, callback));
// Tests null callback
assertThrows(NullPointerException.class,
() -> mRouter2.requestCreateSession(route, controlCategory, executor, null));
() -> mRouter2.registerSessionCallback(executor, null));
}
@Test
public void testUnregisterSessionCallbackWithNullCallback() {
// Tests null callback
assertThrows(NullPointerException.class,
() -> mRouter2.unregisterSessionCallback(null));
}
@Test
public void testRequestCreateSessionWithInvalidArguments() {
MediaRoute2Info route = new MediaRoute2Info.Builder("id", "name").build();
String controlCategory = "controlCategory";
// Tests null route
assertThrows(NullPointerException.class,
() -> mRouter2.requestCreateSession(null, controlCategory));
// Tests null or empty control category
assertThrows(IllegalArgumentException.class,
() -> mRouter2.requestCreateSession(route, null));
assertThrows(IllegalArgumentException.class,
() -> mRouter2.requestCreateSession(route, ""));
}
@Test
@@ -243,7 +256,7 @@ public class MediaRouter2Test {
final CountDownLatch failureLatch = new CountDownLatch(1);
// Create session with this route
SessionCallback callback = new SessionCallback() {
SessionCallback sessionCallback = new SessionCallback() {
@Override
public void onSessionCreated(RouteSessionController controller) {
assertNotNull(controller);
@@ -253,19 +266,27 @@ public class MediaRouter2Test {
}
@Override
public void onSessionCreationFailed() {
public void onSessionCreationFailed(MediaRoute2Info requestedRoute,
String requestedControlCategory) {
failureLatch.countDown();
}
};
// TODO: Remove this once the MediaRouter2 becomes always connected to the service.
mRouter2.registerCallback(mExecutor, new MediaRouter2.RouteCallback());
RouteCallback routeCallback = new RouteCallback();
mRouter2.registerRouteCallback(mExecutor, routeCallback);
mRouter2.requestCreateSession(route, CATEGORY_SAMPLE, mExecutor, callback);
assertTrue(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
try {
mRouter2.registerSessionCallback(mExecutor, sessionCallback);
mRouter2.requestCreateSession(route, CATEGORY_SAMPLE);
assertTrue(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// onSessionCreationFailed should not be called.
assertFalse(failureLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// onSessionCreationFailed should not be called.
assertFalse(failureLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} finally {
mRouter2.unregisterRouteCallback(routeCallback);
mRouter2.unregisterSessionCallback(sessionCallback);
}
}
@Test
@@ -281,31 +302,142 @@ public class MediaRouter2Test {
final CountDownLatch failureLatch = new CountDownLatch(1);
// Create session with this route
SessionCallback callback = new SessionCallback() {
SessionCallback sessionCallback = new SessionCallback() {
@Override
public void onSessionCreated(RouteSessionController controller) {
successLatch.countDown();
}
@Override
public void onSessionCreationFailed() {
public void onSessionCreationFailed(MediaRoute2Info requestedRoute,
String requestedControlCategory) {
assertEquals(route, requestedRoute);
assertTrue(TextUtils.equals(CATEGORY_SAMPLE, requestedControlCategory));
failureLatch.countDown();
}
};
// TODO: Remove this once the MediaRouter2 becomes always connected to the service.
mRouter2.registerCallback(mExecutor, new MediaRouter2.RouteCallback());
RouteCallback routeCallback = new RouteCallback();
mRouter2.registerRouteCallback(mExecutor, routeCallback);
mRouter2.requestCreateSession(route, CATEGORY_SAMPLE, mExecutor, callback);
assertTrue(failureLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
try {
mRouter2.registerSessionCallback(mExecutor, sessionCallback);
mRouter2.requestCreateSession(route, CATEGORY_SAMPLE);
assertTrue(failureLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// onSessionCreated should not be called.
assertFalse(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// onSessionCreated should not be called.
assertFalse(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} finally {
mRouter2.unregisterRouteCallback(routeCallback);
mRouter2.unregisterSessionCallback(sessionCallback);
}
}
@Test
public void testRequestCreateSessionMultipleSessions() throws Exception {
// TODO: Test creating multiple sessions (Check the ID of each controller)
final List<String> sampleControlCategory = new ArrayList<>();
sampleControlCategory.add(CATEGORY_SAMPLE);
final CountDownLatch successLatch = new CountDownLatch(2);
final CountDownLatch failureLatch = new CountDownLatch(1);
final List<RouteSessionController> createdControllers = new ArrayList<>();
// Create session with this route
SessionCallback sessionCallback = new SessionCallback() {
@Override
public void onSessionCreated(RouteSessionController controller) {
createdControllers.add(controller);
successLatch.countDown();
}
@Override
public void onSessionCreationFailed(MediaRoute2Info requestedRoute,
String requestedControlCategory) {
failureLatch.countDown();
}
};
Map<String, MediaRoute2Info> routes = waitAndGetRoutes(sampleControlCategory);
MediaRoute2Info route1 = routes.get(ROUTE_ID1);
MediaRoute2Info route2 = routes.get(ROUTE_ID2);
assertNotNull(route1);
assertNotNull(route2);
// TODO: Remove this once the MediaRouter2 becomes always connected to the service.
RouteCallback routeCallback = new RouteCallback();
mRouter2.registerRouteCallback(mExecutor, routeCallback);
try {
mRouter2.registerSessionCallback(mExecutor, sessionCallback);
mRouter2.requestCreateSession(route1, CATEGORY_SAMPLE);
mRouter2.requestCreateSession(route2, CATEGORY_SAMPLE);
assertTrue(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// onSessionCreationFailed should not be called.
assertFalse(failureLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// Created controllers should have proper info
assertEquals(2, createdControllers.size());
RouteSessionController controller1 = createdControllers.get(0);
RouteSessionController controller2 = createdControllers.get(1);
assertNotEquals(controller1.getSessionId(), controller2.getSessionId());
assertTrue(controller1.getSelectedRoutes().contains(ROUTE_ID1));
assertTrue(controller2.getSelectedRoutes().contains(ROUTE_ID2));
assertTrue(TextUtils.equals(CATEGORY_SAMPLE, controller1.getControlCategory()));
assertTrue(TextUtils.equals(CATEGORY_SAMPLE, controller2.getControlCategory()));
} finally {
mRouter2.unregisterRouteCallback(routeCallback);
mRouter2.unregisterSessionCallback(sessionCallback);
}
}
@Test
public void testSessionCallbackIsNotCalledAfterUnregistered() throws Exception {
final List<String> sampleControlCategory = new ArrayList<>();
sampleControlCategory.add(CATEGORY_SAMPLE);
Map<String, MediaRoute2Info> routes = waitAndGetRoutes(sampleControlCategory);
MediaRoute2Info route = routes.get(ROUTE_ID1);
assertNotNull(route);
final CountDownLatch successLatch = new CountDownLatch(1);
final CountDownLatch failureLatch = new CountDownLatch(1);
// Create session with this route
SessionCallback sessionCallback = new SessionCallback() {
@Override
public void onSessionCreated(RouteSessionController controller) {
successLatch.countDown();
}
@Override
public void onSessionCreationFailed(MediaRoute2Info requestedRoute,
String requestedControlCategory) {
failureLatch.countDown();
}
};
// TODO: Remove this once the MediaRouter2 becomes always connected to the service.
RouteCallback routeCallback = new RouteCallback();
mRouter2.registerRouteCallback(mExecutor, routeCallback);
try {
mRouter2.registerSessionCallback(mExecutor, sessionCallback);
mRouter2.requestCreateSession(route, CATEGORY_SAMPLE);
// Unregisters session callback
mRouter2.unregisterSessionCallback(sessionCallback);
// No session callback methods should be called.
assertFalse(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertFalse(failureLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} finally {
mRouter2.unregisterRouteCallback(routeCallback);
mRouter2.unregisterSessionCallback(sessionCallback);
}
}
// Helper for getting routes easily
@@ -323,8 +455,7 @@ public class MediaRouter2Test {
CountDownLatch latch = new CountDownLatch(1);
// A dummy callback is required to send control category info.
MediaRouter2.RouteCallback
routeCallback = new MediaRouter2.RouteCallback() {
RouteCallback routeCallback = new RouteCallback() {
@Override
public void onRoutesAdded(List<MediaRoute2Info> routes) {
for (int i = 0; i < routes.size(); i++) {
@@ -337,20 +468,19 @@ public class MediaRouter2Test {
};
mRouter2.setControlCategories(controlCategories);
mRouter2.registerCallback(mExecutor, routeCallback);
mRouter2.registerRouteCallback(mExecutor, routeCallback);
try {
latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
return createRouteMap(mRouter2.getRoutes());
} finally {
mRouter2.unregisterCallback(routeCallback);
mRouter2.unregisterRouteCallback(routeCallback);
}
}
void awaitOnRouteChanged(Runnable task, String routeId,
Predicate<MediaRoute2Info> predicate) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
MediaRouter2.RouteCallback
routeCallback = new MediaRouter2.RouteCallback() {
RouteCallback routeCallback = new RouteCallback() {
@Override
public void onRoutesChanged(List<MediaRoute2Info> changed) {
MediaRoute2Info route = createRouteMap(changed).get(routeId);
@@ -359,12 +489,12 @@ public class MediaRouter2Test {
}
}
};
mRouter2.registerCallback(mExecutor, routeCallback);
mRouter2.registerRouteCallback(mExecutor, routeCallback);
try {
task.run();
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} finally {
mRouter2.unregisterCallback(routeCallback);
mRouter2.unregisterRouteCallback(routeCallback);
}
}
}

View File

@@ -384,12 +384,12 @@ public class MediaRouterManagerTest {
};
mManager.registerCallback(mExecutor, managerCallback);
mRouter2.setControlCategories(controlCategories);
mRouter2.registerCallback(mExecutor, routeCallback);
mRouter2.registerRouteCallback(mExecutor, routeCallback);
try {
latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
return createRouteMap(mManager.getAvailableRoutes(mPackageName));
} finally {
mRouter2.unregisterCallback(routeCallback);
mRouter2.unregisterRouteCallback(routeCallback);
mManager.unregisterCallback(managerCallback);
}
}
@@ -432,7 +432,7 @@ public class MediaRouterManagerTest {
private void addRouterCallback(RouteCallback routeCallback) {
mRouteCallbacks.add(routeCallback);
mRouter2.registerCallback(mExecutor, routeCallback);
mRouter2.registerRouteCallback(mExecutor, routeCallback);
}
private void clearCallbacks() {
@@ -442,7 +442,7 @@ public class MediaRouterManagerTest {
mManagerCallbacks.clear();
for (RouteCallback routeCallback : mRouteCallbacks) {
mRouter2.unregisterCallback(routeCallback);
mRouter2.unregisterRouteCallback(routeCallback);
}
mRouteCallbacks.clear();
}