Loading media/java/android/media/IMediaRouter2Client.aidl +4 −2 Original line number Diff line number Diff line Loading @@ -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); } media/java/android/media/MediaRoute2Info.java +1 −1 Original line number Diff line number Diff line Loading @@ -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) { Loading media/java/android/media/MediaRouter2.java +131 −83 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading Loading @@ -137,6 +139,7 @@ public class MediaRouter2 { } } } //TODO: Is it thread-safe? record.notifyRoutes(); //TODO: Update discovery request here. Loading Loading @@ -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; } /** Loading Loading @@ -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)); } } Loading @@ -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) {} } Loading @@ -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)); } } } Loading @@ -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)); } } } media/java/android/media/MediaRouter2Manager.java +3 −1 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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 { Loading media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java +42 −24 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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)); Loading Loading @@ -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); Loading @@ -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); Loading @@ -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 Loading
media/java/android/media/IMediaRouter2Client.aidl +4 −2 Original line number Diff line number Diff line Loading @@ -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); }
media/java/android/media/MediaRoute2Info.java +1 −1 Original line number Diff line number Diff line Loading @@ -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) { Loading
media/java/android/media/MediaRouter2.java +131 −83 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading Loading @@ -137,6 +139,7 @@ public class MediaRouter2 { } } } //TODO: Is it thread-safe? record.notifyRoutes(); //TODO: Update discovery request here. Loading Loading @@ -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; } /** Loading Loading @@ -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)); } } Loading @@ -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) {} } Loading @@ -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)); } } } Loading @@ -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)); } } }
media/java/android/media/MediaRouter2Manager.java +3 −1 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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 { Loading
media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java +42 −24 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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)); Loading Loading @@ -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); Loading @@ -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); Loading @@ -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