Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a64ca9ec authored by Hyundo Moon's avatar Hyundo Moon
Browse files

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
parent 7b36ffb0
Loading
Loading
Loading
Loading
+117 −32
Original line number Diff line number Diff line
@@ -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;
        }
    }
+3 −3
Original line number Diff line number Diff line
@@ -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)
+166 −36
Original line number Diff line number Diff line
@@ -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;
@@ -204,30 +206,41 @@ public class MediaRouter2Test {
                (route -> route.getVolume() == originalVolume));
    }

    @Test
    public void testRegisterSessionCallbackWithInvalidArguments() {
        Executor executor = mExecutor;
        SessionCallback callback = new SessionCallback();

        // Tests null executor
        assertThrows(NullPointerException.class,
                () -> mRouter2.registerSessionCallback(null, callback));

        // Tests null callback
        assertThrows(NullPointerException.class,
                () -> 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";
        Executor executor = mExecutor;
        SessionCallback callback = new SessionCallback();

        // Tests null route
        assertThrows(NullPointerException.class,
                () -> mRouter2.requestCreateSession(null, controlCategory, executor, callback));
                () -> mRouter2.requestCreateSession(null, controlCategory));

        // Tests null or empty control category
        assertThrows(IllegalArgumentException.class,
                () -> mRouter2.requestCreateSession(route, null, executor, callback));
                () -> mRouter2.requestCreateSession(route, null));
        assertThrows(IllegalArgumentException.class,
                () -> mRouter2.requestCreateSession(route, "", executor, callback));

        // Tests null executor
        assertThrows(NullPointerException.class,
                () -> mRouter2.requestCreateSession(route, controlCategory, null, callback));

        // Tests null callback
        assertThrows(NullPointerException.class,
                () -> mRouter2.requestCreateSession(route, controlCategory, executor, null));
                () -> 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);
        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));
        } 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);
        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));
        } 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);
        }
    }
}
+4 −4

File changed.

Preview size limit exceeded, changes collapsed.