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

Commit 61094d7c authored by Kyunglyul Hyun's avatar Kyunglyul Hyun Committed by Android (Google) Code Review
Browse files

Merge "MediaRouter2: Revise MediaRouter2.Callback.onRoute**"

parents 591f29d8 7af7301e
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