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

Commit 7af7301e authored by Kyunglyul Hyun's avatar Kyunglyul Hyun
Browse files

MediaRouter2: Revise MediaRouter2.Callback.onRoute**

This CL revised callbacks for MediaRouter2 so that it gives
a list instead of a single route.
This will effectively reduce the number of invokings.

This CL also fixes binder call between media router service and media
router such that media router service notifies only changes of routes
instead of all provider information.

Following CLs will update manager callbacks as well.

Test: atest mediaroutertest
Change-Id: Ibbc23aa5f12f5450d44c81756e3fcefe3e0d7f4b
parent e1223503
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -16,12 +16,14 @@

package android.media;

import android.media.MediaRoute2ProviderInfo;
import android.media.MediaRoute2Info;

/**
 * @hide
 */
oneway interface IMediaRouter2Client {
    void notifyRestoreRoute();
    void notifyProviderInfosUpdated(in List<MediaRoute2ProviderInfo> providers);
    void notifyRoutesAdded(in List<MediaRoute2Info> routes);
    void notifyRoutesRemoved(in List<MediaRoute2Info> routes);
    void notifyRoutesChanged(in List<MediaRoute2Info> routes);
}
+1 −1
Original line number Diff line number Diff line
@@ -314,7 +314,7 @@ public final class MediaRoute2Info implements Parcelable {
        List<String> mSupportedCategories;
        int mVolume;
        int mVolumeMax;
        int mVolumeHandling;
        int mVolumeHandling = PLAYBACK_VOLUME_FIXED;
        Bundle mExtras;

        public Builder(@NonNull String id, @NonNull String name) {
+131 −83
Original line number Diff line number Diff line
@@ -33,7 +33,9 @@ import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
@@ -51,23 +53,23 @@ public class MediaRouter2 {
    @GuardedBy("sLock")
    private static MediaRouter2 sInstance;

    private Context mContext;
    private final Context mContext;
    private final IMediaRouterService mMediaRouterService;

    private final CopyOnWriteArrayList<CallbackRecord> mCallbackRecords =
            new CopyOnWriteArrayList<>();
    @GuardedBy("sLock")
    private List<String> mControlCategories = Collections.emptyList();
    @GuardedBy("sLock")
    private Client mClient;

    private final String mPackageName;
    final Handler mHandler;
    private final Map<String, MediaRoute2Info> mRoutes = new HashMap<>();

    List<MediaRoute2ProviderInfo> mProviders = Collections.emptyList();
    volatile List<MediaRoute2Info> mRoutes = Collections.emptyList();
    private volatile List<String> mControlCategories = Collections.emptyList();

    MediaRoute2Info mSelectedRoute;
    private MediaRoute2Info mSelectedRoute;
    @GuardedBy("sLock")
    private Client mClient;

    final Handler mHandler;
    volatile List<MediaRoute2Info> mFilteredRoutes = Collections.emptyList();

    /**
     * Gets an instance of the media router associated with the context.
@@ -137,6 +139,7 @@ public class MediaRouter2 {
                }
            }
        }
        //TODO: Is it thread-safe?
        record.notifyRoutes();

        //TODO: Update discovery request here.
@@ -181,31 +184,22 @@ public class MediaRouter2 {
    public void setControlCategories(@NonNull Collection<String> controlCategories) {
        Objects.requireNonNull(controlCategories, "control categories must not be null");

        Client client;
        List<String> newControlCategories = new ArrayList<>(controlCategories);
        synchronized (sLock) {
            mControlCategories = newControlCategories;
            client = mClient;
        }
        if (client != null) {
            try {
                mMediaRouterService.setControlCategories2(client, newControlCategories);
            } catch (RemoteException ex) {
                Log.e(TAG, "Unable to set control categories.", ex);
            }
        }
        mHandler.sendMessage(obtainMessage(MediaRouter2::refreshAndNotifyRoutes, this));
        // To ensure invoking callbacks correctly according to control categories
        mHandler.sendMessage(obtainMessage(MediaRouter2::setControlCategoriesOnHandler,
                MediaRouter2.this, new ArrayList<>(controlCategories)));
    }


    /**
     * Gets the list of {@link MediaRoute2Info routes} currently known to the media router.
     * Gets the unmodifiable list of {@link MediaRoute2Info routes} currently
     * known to the media router.
     *
     * @return the list of routes that support at least one of the control categories set by
     * the application
     */
    @NonNull
    public List<MediaRoute2Info> getRoutes() {
        return mRoutes;
        return mFilteredRoutes;
    }

    /**
@@ -326,77 +320,121 @@ public class MediaRouter2 {
        return -1;
    }

    void onProviderInfosUpdated(List<MediaRoute2ProviderInfo> providers) {
        if (providers == null) {
            Log.w(TAG, "Providers info is null.");
            return;
        }
    private void setControlCategoriesOnHandler(List<String> newControlCategories) {
        List<String> prevControlCategories = mControlCategories;
        List<MediaRoute2Info> addedRoutes = new ArrayList<>();
        List<MediaRoute2Info> removedRoutes = new ArrayList<>();
        List<MediaRoute2Info> filteredRoutes = new ArrayList<>();

        mProviders = providers;
        refreshAndNotifyRoutes();
        mControlCategories = newControlCategories;
        Client client;
        synchronized (sLock) {
            client = mClient;
        }
        if (client != null) {
            try {
                mMediaRouterService.setControlCategories2(client, mControlCategories);
            } catch (RemoteException ex) {
                Log.e(TAG, "Unable to set control categories.", ex);
            }
        }

    void refreshAndNotifyRoutes() {
        ArrayList<MediaRoute2Info> routes = new ArrayList<>();

        List<String> controlCategories;
        synchronized (sLock) {
            controlCategories = mControlCategories;
        for (MediaRoute2Info route : mRoutes.values()) {
            boolean preSupported = route.supportsControlCategory(prevControlCategories);
            boolean postSupported = route.supportsControlCategory(newControlCategories);
            if (postSupported) {
                filteredRoutes.add(route);
            }
            if (preSupported == postSupported) {
                continue;
            }
            if (preSupported) {
                removedRoutes.add(route);
            } else {
                addedRoutes.add(route);
            }
        }
        mFilteredRoutes = Collections.unmodifiableList(filteredRoutes);

        for (MediaRoute2ProviderInfo provider : mProviders) {
            updateProvider(provider, controlCategories, routes);
        if (removedRoutes.size() > 0) {
            notifyRoutesRemoved(removedRoutes);
        }
        if (addedRoutes.size() > 0) {
            notifyRoutesAdded(addedRoutes);
        }
    }

        //TODO: Can orders be changed?
        if (!Objects.equals(mRoutes, routes)) {
            mRoutes = Collections.unmodifiableList(routes);
            notifyRouteListChanged(mRoutes);
    void addRoutesOnHandler(List<MediaRoute2Info> routes) {
        List<MediaRoute2Info> addedRoutes = new ArrayList<>();
        for (MediaRoute2Info route : routes) {
            mRoutes.put(route.getUniqueId(), route);
            if (route.supportsControlCategory(mControlCategories)) {
                addedRoutes.add(route);
            }
        }
        if (addedRoutes.size() > 0) {
            refreshFilteredRoutes();
            notifyRoutesAdded(addedRoutes);
        }
    }

    void updateProvider(MediaRoute2ProviderInfo provider, List<String> controlCategories,
            List<MediaRoute2Info> outRoutes) {
        if (provider == null || !provider.isValid()) {
            Log.w(TAG, "Ignoring invalid provider : " + provider);
            return;
    void removeRoutesOnHandler(List<MediaRoute2Info> routes) {
        List<MediaRoute2Info> removedRoutes = new ArrayList<>();
        for (MediaRoute2Info route : routes) {
            mRoutes.remove(route.getUniqueId());
            if (route.supportsControlCategory(mControlCategories)) {
                removedRoutes.add(route);
            }
        }
        if (removedRoutes.size() > 0) {
            refreshFilteredRoutes();
            notifyRoutesRemoved(removedRoutes);
        }
    }

        final Collection<MediaRoute2Info> routes = provider.getRoutes();
    void changeRoutesOnHandler(List<MediaRoute2Info> routes) {
        List<MediaRoute2Info> changedRoutes = new ArrayList<>();
        for (MediaRoute2Info route : routes) {
            if (!route.isValid()) {
                Log.w(TAG, "Ignoring invalid route : " + route);
                continue;
            mRoutes.put(route.getUniqueId(), route);
            if (route.supportsControlCategory(mControlCategories)) {
                changedRoutes.add(route);
            }
        }
        if (changedRoutes.size() > 0) {
            refreshFilteredRoutes();
            notifyRoutesChanged(changedRoutes);
        }
            if (!route.supportsControlCategory(controlCategories)) {
                continue;
    }
            MediaRoute2Info preRoute = findRouteById(route.getId());
            if (!route.equals(preRoute)) {
                notifyRouteChanged(route);

    private void refreshFilteredRoutes() {
        List<MediaRoute2Info> filteredRoutes = new ArrayList<>();

        for (MediaRoute2Info route : mRoutes.values()) {
            if (route.supportsControlCategory(mControlCategories)) {
                filteredRoutes.add(route);
            }
            outRoutes.add(route);
        }
        mFilteredRoutes = Collections.unmodifiableList(filteredRoutes);
    }

    MediaRoute2Info findRouteById(String id) {
        for (MediaRoute2Info route : mRoutes) {
            if (route.getId().equals(id)) return route;
    private void notifyRoutesAdded(List<MediaRoute2Info> routes) {
        for (CallbackRecord record: mCallbackRecords) {
            record.mExecutor.execute(
                    () -> record.mCallback.onRoutesAdded(routes));
        }
        return null;
    }

    void notifyRouteListChanged(List<MediaRoute2Info> routes) {
    private void notifyRoutesRemoved(List<MediaRoute2Info> routes) {
        for (CallbackRecord record: mCallbackRecords) {
            record.mExecutor.execute(
                    () -> record.mCallback.onRoutesChanged(routes));
                    () -> record.mCallback.onRoutesRemoved(routes));
        }
    }

    void notifyRouteChanged(MediaRoute2Info route) {
    private void notifyRoutesChanged(List<MediaRoute2Info> routes) {
        for (CallbackRecord record: mCallbackRecords) {
            record.mExecutor.execute(
                    () -> record.mCallback.onRouteChanged(route));
                    () -> record.mCallback.onRoutesChanged(routes));
        }
    }

@@ -405,23 +443,22 @@ public class MediaRouter2 {
     */
    public static class Callback {
        //TODO: clean up these callbacks
        /**
         * Called when a route is added.
         */
        public void onRouteAdded(MediaRoute2Info routeInfo) {}

        /**
         * Called when a route is changed.
         * Called when routes are added.
         * @param routes the list of routes that have been added. It's never empty.
         */
        public void onRouteChanged(MediaRoute2Info routeInfo) {}
        public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {}

        /**
         * Called when a route is removed.
         * Called when routes are removed.
         * @param routes the list of routes that have been removed. It's never empty.
         */
        public void onRouteRemoved(MediaRoute2Info routeInfo) {}
        public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {}

        /**
         * Called when the list of routes is changed.
         * Called when routes are changed.
         * @param routes the list of routes that have been changed. It's never empty.
         */
        public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {}
    }
@@ -436,11 +473,10 @@ public class MediaRouter2 {
        }

        void notifyRoutes() {
            final List<MediaRoute2Info> routes = mRoutes;
            final List<MediaRoute2Info> routes = mFilteredRoutes;
            // notify only when bound to media router service.
            //TODO: Correct the condition when control category, default route, .. are finalized.
            if (routes.size() > 0) {
                mExecutor.execute(() -> mCallback.onRoutesChanged(routes));
                mExecutor.execute(() -> mCallback.onRoutesAdded(routes));
            }
        }
    }
@@ -450,9 +486,21 @@ public class MediaRouter2 {
        public void notifyRestoreRoute() throws RemoteException {}

        @Override
        public void notifyProviderInfosUpdated(List<MediaRoute2ProviderInfo> info) {
            mHandler.sendMessage(obtainMessage(MediaRouter2::onProviderInfosUpdated,
                    MediaRouter2.this, info));
        public void notifyRoutesAdded(List<MediaRoute2Info> routes) {
            mHandler.sendMessage(obtainMessage(MediaRouter2::addRoutesOnHandler,
                    MediaRouter2.this, routes));
        }

        @Override
        public void notifyRoutesRemoved(List<MediaRoute2Info> routes) {
            mHandler.sendMessage(obtainMessage(MediaRouter2::removeRoutesOnHandler,
                    MediaRouter2.this, routes));
        }

        @Override
        public void notifyRoutesChanged(List<MediaRoute2Info> routes) {
            mHandler.sendMessage(obtainMessage(MediaRouter2::changeRoutesOnHandler,
                    MediaRouter2.this, routes));
        }
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -67,7 +67,7 @@ public class MediaRouter2Manager {
    @NonNull
    List<MediaRoute2Info> mRoutes = Collections.emptyList();
    @NonNull
    ConcurrentMap<String, List<String>> mControlCategoryMap = new ConcurrentHashMap<>();
    final ConcurrentMap<String, List<String>> mControlCategoryMap = new ConcurrentHashMap<>();

    /**
     * Gets an instance of media router manager that controls media route of other applications.
@@ -427,6 +427,8 @@ public class MediaRouter2Manager {
         * A client may refresh available routes for each application.
         */
        public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {}

        //TODO: add onControlCategoriesChanged to notify available routes are changed
    }

    final class CallbackRecord {
+42 −24
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
@@ -272,29 +273,28 @@ public class MediaRouterManagerTest {

    @Test
    public void testControlVolumeWithRouter() throws Exception {
        MediaRouter2.Callback mockCallback = mock(MediaRouter2.Callback.class);

        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(CONTROL_CATEGORIES_ALL);
        mRouter2.registerCallback(mExecutor, mockCallback);

        MediaRoute2Info volRoute = routes.get(ROUTE_ID_VARIABLE_VOLUME);
        int originalVolume = volRoute.getVolume();
        int deltaVolume = (originalVolume == volRoute.getVolumeMax() ? -1 : 1);
        int targetVolume = originalVolume + deltaVolume;

        mRouter2.requestSetVolume(volRoute, targetVolume);
        verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce())
                .onRouteChanged(argThat(route ->
                        route.getId().equals(volRoute.getId())
                                && route.getVolume() == targetVolume));

        mRouter2.requestUpdateVolume(volRoute, -deltaVolume);
        verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce())
                .onRouteChanged(argThat(route ->
                        route.getId().equals(volRoute.getId())
                                && route.getVolume() == originalVolume));

        mRouter2.unregisterCallback(mockCallback);
        CountDownLatch latch1 = new CountDownLatch(1);
        MediaRouter2.Callback callback1 =
                createVolumeChangeCallback(ROUTE_ID_VARIABLE_VOLUME,
                        originalVolume + deltaVolume, latch1);
        mRouter2.registerCallback(mExecutor, callback1);
        mRouter2.requestUpdateVolume(volRoute, deltaVolume);
        assertTrue(latch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        mRouter2.unregisterCallback(callback1);

        CountDownLatch latch2 = new CountDownLatch(1);
        MediaRouter2.Callback callback2 =
                createVolumeChangeCallback(ROUTE_ID_VARIABLE_VOLUME, originalVolume, latch2);
        mRouter2.registerCallback(mExecutor, callback2);
        mRouter2.requestSetVolume(volRoute, originalVolume);
        assertTrue(latch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        mRouter2.unregisterCallback(callback1);
    }

    @Test
@@ -312,13 +312,13 @@ public class MediaRouterManagerTest {
        int targetVolume = originalVolume + deltaVolume;

        mManager.requestSetVolume(volRoute, targetVolume);
        verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce())
        verify(managerCallback, timeout(TIMEOUT_MS).atLeastOnce())
                .onRouteChanged(argThat(route ->
                        route.getId().equals(volRoute.getId())
                                && route.getVolume() == targetVolume));

        mManager.requestUpdateVolume(volRoute, -deltaVolume);
        verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce())
        verify(managerCallback, timeout(TIMEOUT_MS).atLeastOnce())
                .onRouteChanged(argThat(route ->
                        route.getId().equals(volRoute.getId())
                                && route.getVolume() == originalVolume));
@@ -347,14 +347,14 @@ public class MediaRouterManagerTest {
        CountDownLatch latch = new CountDownLatch(1);
        MediaRouter2.Callback callback = new MediaRouter2.Callback() {
            @Override
            public void onRoutesChanged(List<MediaRoute2Info> routes) {
                if (routes.size() > 0) latch.countDown();
            public void onRoutesAdded(List<MediaRoute2Info> added) {
                if (added.size() > 0) latch.countDown();
            }
        };
        mRouter2.setControlCategories(controlCategories);
        mRouter2.registerCallback(mExecutor, callback);
        try {
            latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
            return createRouteMap(mRouter2.getRoutes());
        } finally {
            mRouter2.unregisterCallback(callback);
@@ -370,14 +370,18 @@ public class MediaRouterManagerTest {
        MediaRouter2Manager.Callback managerCallback = new MediaRouter2Manager.Callback() {
            @Override
            public void onRoutesChanged(List<MediaRoute2Info> routes) {
                if (routes.size() > 0) latch.countDown();
                if (routes.size() > 0) {
                    latch.countDown();
                }
            }
        };
        mManager.registerCallback(mExecutor, managerCallback);
        mRouter2.setControlCategories(controlCategories);
        mRouter2.registerCallback(mExecutor, routerCallback);
        try {
            latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
            assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
            //TODO: currently this returns empty list occasionally.
            //Maybe due to control category is not set yet
            return createRouteMap(mManager.getAvailableRoutes(mPackageName));
        } finally {
            mRouter2.unregisterCallback(routerCallback);
@@ -385,6 +389,20 @@ public class MediaRouterManagerTest {
        }
    }

    MediaRouter2.Callback createVolumeChangeCallback(String routeId,
            int targetVolume, CountDownLatch latch) {
        MediaRouter2.Callback callback = new MediaRouter2.Callback() {
            @Override
            public void onRoutesChanged(List<MediaRoute2Info> changed) {
                MediaRoute2Info volRoute = createRouteMap(changed).get(routeId);
                if (volRoute != null && volRoute.getVolume() == targetVolume) {
                    latch.countDown();
                }
            }
        };
        return callback;
    }

    // Helper for getting routes easily
    static Map<String, MediaRoute2Info> createRouteMap(List<MediaRoute2Info> routes) {
        Map<String, MediaRoute2Info> routeMap = new HashMap<>();
Loading