No-op when selecting the selected or pending selected route
If apps call RouteInfo#select() multiple times, we should be
no-op when the to-be-selected route is already a selected route
or there is a pending route selection.
Bug: b/425408032
Test: manually test it with sample apps and presubmit tests
(cherry picked from https://android-review.googlesource.com/q/commit:02314738c04b57dc4efcb76f171956f0238530b4)
Merged-In: I025d473e8bacea1fc90988f6fa78a657b4989c39
Change-Id: I025d473e8bacea1fc90988f6fa78a657b4989c39
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2Test.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2Test.java
index 2521b1b..766c5e7 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2Test.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/MediaRouter2Test.java
@@ -31,6 +31,7 @@
import android.os.Bundle;
import android.os.Messenger;
import android.text.TextUtils;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.mediarouter.media.MediaRouter.RouteInfo;
@@ -64,9 +65,11 @@
private Context mContext;
private MediaRouter mRouter;
- private MediaRouter.Callback mPlaceholderCallback = new MediaRouter.Callback() {};
- StubMediaRouteProviderService mService;
- StubMediaRouteProviderService.StubMediaRouteProvider mProvider;
+ private final MediaRouter.Callback mPlaceholderCallback = new MediaRouter.Callback() {};
+ StubMediaRouteProviderService mMr1ProviderService;
+ StubMediaRouteProviderService.StubMediaRouteProvider mMr1Provider;
+ StubMediaRoute2ProviderService mMr2ProviderService;
+ StubMediaRoute2ProviderService.StubMediaRoute2Provider mMr2Provider;
MediaRouteProviderService.MediaRouteProviderServiceImplApi30 mServiceImpl;
MediaRoute2ProviderServiceAdapter mMr2ProviderServiceAdapter;
@@ -98,22 +101,41 @@
new PollingCheck(TIMEOUT_MS) {
@Override
protected boolean check() {
- mService = StubMediaRouteProviderService.getInstance();
- if (mService != null && mService.getMediaRouteProvider() != null) {
- mProvider = (StubMediaRouteProviderService.StubMediaRouteProvider)
- mService.getMediaRouteProvider();
- mServiceImpl = (MediaRouteProviderService.MediaRouteProviderServiceImplApi30)
- mService.mImpl;
+ mMr1ProviderService = StubMediaRouteProviderService.getInstance();
+ boolean isMr1ProviderCreated = false;
+ if (mMr1ProviderService != null
+ && mMr1ProviderService.getMediaRouteProvider() != null) {
+ mMr1Provider =
+ (StubMediaRouteProviderService.StubMediaRouteProvider)
+ mMr1ProviderService.getMediaRouteProvider();
+ mServiceImpl =
+ (MediaRouteProviderService.MediaRouteProviderServiceImplApi30)
+ mMr1ProviderService.mImpl;
mMr2ProviderServiceAdapter = mServiceImpl.mMR2ProviderServiceAdapter;
- return mMr2ProviderServiceAdapter != null;
+ isMr1ProviderCreated = mMr2ProviderServiceAdapter != null;
}
- return false;
+
+ mMr2ProviderService = StubMediaRoute2ProviderService.getInstance();
+ boolean isMr2ProviderCreated = false;
+ if (mMr2ProviderService != null
+ && mMr2ProviderService.getMediaRouteProvider() != null) {
+ mMr2Provider =
+ (StubMediaRoute2ProviderService.StubMediaRoute2Provider)
+ mMr2ProviderService.getMediaRouteProvider();
+ isMr2ProviderCreated = mMr2Provider != null;
+ }
+
+ return isMr1ProviderCreated && isMr2ProviderCreated;
}
}.run();
- getInstrumentation().runOnMainSync(() -> {
- mProvider.initializeRoutes();
- mProvider.publishRoutes();
- });
+ getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mMr1Provider.initializeRoutes();
+ mMr1Provider.publishRoutes();
+ mMr2Provider.initializeRoutes();
+ mMr2Provider.publishRoutes();
+ });
}
@After
@@ -139,42 +161,196 @@
@Test
@MediumTest
+ public void selectRoute_withSelectedMr1Route_shouldBeNoOp() throws Exception {
+ String descriptorId = StubMediaRouteProviderService.ROUTE_ID1;
+ waitForRoutesAdded(descriptorId);
+ assertNotNull(mRoutes);
+
+ // Select the route for the first time.
+ waitForRouteSelected(descriptorId, descriptorId, /* routeSelected= */ true);
+
+ // Wait for a session being created.
+ PollingCheck.waitFor(
+ TIMEOUT_MS, () -> !mMr2ProviderServiceAdapter.getAllSessionInfo().isEmpty());
+
+ // Select the route for the second time, which should be no op.
+ waitForRouteSelected(descriptorId, descriptorId, /* routeSelected= */ false);
+
+ // Stop casting the session before casting to the same route again.
+ waitForRouteUnselected(descriptorId, descriptorId);
+
+ // Wait for a session being released.
+ PollingCheck.waitFor(
+ TIMEOUT_MS, () -> mMr2ProviderServiceAdapter.getAllSessionInfo().isEmpty());
+
+ // Select the route for casting again.
+ waitForRouteSelected(descriptorId, descriptorId, /* routeSelected= */ true);
+ }
+
+ @Test
+ @MediumTest
+ public void selectRoute_withSelectedMr2Route_shouldBeNoOp() throws Exception {
+ String descriptorId = StubMediaRoute2ProviderService.MR2_ROUTE_ID1;
+ String mr2DescriptorId = getMediaRoute2DescriptorId(descriptorId);
+ waitForRoutesAdded(mr2DescriptorId);
+ assertNotNull(mRoutes);
+
+ // Select the route for the first time.
+ waitForRouteSelected(
+ mr2DescriptorId,
+ StubMediaRoute2ProviderService.ROUTE_ID_GROUP,
+ /* routeSelected= */ true);
+
+ assertEquals(1, mMr2Provider.getNumberOfCreatedControllers(descriptorId));
+
+ // Select the route for the second time, which should be no op.
+ waitForRouteSelected(
+ mr2DescriptorId,
+ StubMediaRoute2ProviderService.ROUTE_ID_GROUP,
+ /* routeSelected= */ false);
+
+ // Check that only one dynamic group route controller is created.
+ assertEquals(1, mMr2Provider.getNumberOfCreatedControllers(descriptorId));
+
+ // Stop casting the session before casting to the same route again.
+ waitForRouteUnselected(mr2DescriptorId, StubMediaRoute2ProviderService.ROUTE_ID_GROUP);
+ // Wait for the route controller is removed from the media route provider.
+ PollingCheck.waitFor(
+ TIMEOUT_MS, () -> mMr2Provider.getNumberOfCreatedControllers(descriptorId) == 0);
+
+ assertEquals(0, mMr2Provider.getNumberOfCreatedControllers(descriptorId));
+
+ // Select the route for casting again.
+ waitForRouteSelected(
+ mr2DescriptorId,
+ StubMediaRoute2ProviderService.ROUTE_ID_GROUP,
+ /* routeSelected= */ true);
+
+ assertEquals(1, mMr2Provider.getNumberOfCreatedControllers(descriptorId));
+
+ // Unselect the route to prevent it interrupts other tests.
+ waitForRouteUnselected(mr2DescriptorId, StubMediaRoute2ProviderService.ROUTE_ID_GROUP);
+ // Wait for the route controller is removed from the media route provider.
+ PollingCheck.waitFor(
+ TIMEOUT_MS, () -> mMr2Provider.getNumberOfCreatedControllers(descriptorId) == 0);
+ }
+
+ @Test
+ @MediumTest
+ public void selectRoute_withSelectingMr2Route_shouldBeNoOp() throws Exception {
+ String descriptorId = StubMediaRoute2ProviderService.MR2_ROUTE_ID1;
+ String mr2DescriptorId = getMediaRoute2DescriptorId(descriptorId);
+ waitForRoutesAdded(mr2DescriptorId);
+ assertNotNull(mRoutes);
+
+ RouteInfo routeToSelect = mRoutes.get(mr2DescriptorId);
+ assertNotNull(routeToSelect);
+
+ CountDownLatch onRouteSelectedLatch = new CountDownLatch(2);
+ MediaRouter.Callback callback =
+ new MediaRouter.Callback() {
+ @Override
+ public void onRouteSelected(
+ @NonNull MediaRouter router,
+ @NonNull RouteInfo selectedRoute,
+ int reason,
+ @NonNull RouteInfo requestedRoute) {
+ Log.i(
+ TAG,
+ "onRouteSelected with selectedRoute = "
+ + selectedRoute
+ + ", requestedRoute = "
+ + requestedRoute
+ + ", reason = "
+ + reason);
+ if (TextUtils.equals(
+ selectedRoute.getDescriptorId(),
+ StubMediaRoute2ProviderService.ROUTE_ID_GROUP)
+ && reason == MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
+ onRouteSelectedLatch.countDown();
+ }
+ }
+ };
+ addCallback(callback);
+
+ // Select the same route twice.
+ getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mRouter.selectRoute(routeToSelect);
+ mRouter.selectRoute(routeToSelect);
+ });
+
+ // Check that only one dynamic group route controller is created.
+ assertFalse(onRouteSelectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(1, onRouteSelectedLatch.getCount());
+ assertEquals(1, mMr2Provider.getNumberOfCreatedControllers(descriptorId));
+
+ // Stop casting the session before casting to the same route again.
+ waitForRouteUnselected(mr2DescriptorId, StubMediaRoute2ProviderService.ROUTE_ID_GROUP);
+ // Wait for the route controller is removed from the media route provider.
+ PollingCheck.waitFor(
+ TIMEOUT_MS, () -> mMr2Provider.getNumberOfCreatedControllers(descriptorId) == 0);
+
+ assertEquals(0, mMr2Provider.getNumberOfCreatedControllers(descriptorId));
+
+ // Select the route for casting again.
+ waitForRouteSelected(
+ mr2DescriptorId,
+ StubMediaRoute2ProviderService.ROUTE_ID_GROUP,
+ /* routeSelected= */ true);
+
+ assertEquals(1, mMr2Provider.getNumberOfCreatedControllers(descriptorId));
+
+ // Unselect the route to prevent it interrupts other tests.
+ waitForRouteUnselected(mr2DescriptorId, StubMediaRoute2ProviderService.ROUTE_ID_GROUP);
+ // Wait for the route controller is removed from the media route provider.
+ PollingCheck.waitFor(
+ TIMEOUT_MS, () -> mMr2Provider.getNumberOfCreatedControllers(descriptorId) == 0);
+ }
+
+ @Test
+ @MediumTest
public void selectFromMr1AndStopFromSystem_unselect() throws Exception {
CountDownLatch onRouteSelectedLatch = new CountDownLatch(1);
CountDownLatch onRouteUnselectedLatch = new CountDownLatch(1);
CountDownLatch onRouteEnabledLatch = new CountDownLatch(1);
String descriptorId = StubMediaRouteProviderService.ROUTE_ID1;
- addCallback(new MediaRouter.Callback() {
- @Override
- public void onRouteSelected(@NonNull MediaRouter router,
- @NonNull RouteInfo selectedRoute, int reason,
- @NonNull RouteInfo requestedRoute) {
- if (TextUtils.equals(selectedRoute.getDescriptorId(), descriptorId)
- && reason == MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
- onRouteSelectedLatch.countDown();
- }
- }
+ addCallback(
+ new MediaRouter.Callback() {
+ @Override
+ public void onRouteSelected(
+ @NonNull MediaRouter router,
+ @NonNull RouteInfo selectedRoute,
+ int reason,
+ @NonNull RouteInfo requestedRoute) {
+ if (TextUtils.equals(selectedRoute.getDescriptorId(), descriptorId)
+ && reason == MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
+ onRouteSelectedLatch.countDown();
+ }
+ }
- @Override
- public void onRouteUnselected(
- @NonNull MediaRouter router, @NonNull RouteInfo route, int reason) {
- if (TextUtils.equals(route.getDescriptorId(), descriptorId)
- && reason == MediaRouter.UNSELECT_REASON_STOPPED) {
- onRouteUnselectedLatch.countDown();
- }
- }
+ @Override
+ public void onRouteUnselected(
+ @NonNull MediaRouter router, @NonNull RouteInfo route, int reason) {
+ if (TextUtils.equals(route.getDescriptorId(), descriptorId)
+ && reason == MediaRouter.UNSELECT_REASON_STOPPED) {
+ onRouteUnselectedLatch.countDown();
+ }
+ }
- @Override
- public void onRouteChanged(@NonNull MediaRouter router, @NonNull RouteInfo route) {
- if (onRouteUnselectedLatch.getCount() == 0
- && TextUtils.equals(route.getDescriptorId(), descriptorId)
- && route.isEnabled()) {
- onRouteEnabledLatch.countDown();
- }
- }
- });
- waitForRoutesAdded();
+ @Override
+ public void onRouteChanged(
+ @NonNull MediaRouter router, @NonNull RouteInfo route) {
+ if (onRouteUnselectedLatch.getCount() == 0
+ && TextUtils.equals(route.getDescriptorId(), descriptorId)
+ && route.isEnabled()) {
+ onRouteEnabledLatch.countDown();
+ }
+ }
+ });
+ waitForRoutesAdded(descriptorId);
assertNotNull(mRoutes);
RouteInfo routeToSelect = mRoutes.get(descriptorId);
@@ -184,14 +360,16 @@
assertTrue(onRouteSelectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// Wait for a session being created.
- PollingCheck.waitFor(TIMEOUT_MS,
- () -> !mMr2ProviderServiceAdapter.getAllSessionInfo().isEmpty());
- //TODO: Find a correct session info
+ PollingCheck.waitFor(
+ TIMEOUT_MS, () -> !mMr2ProviderServiceAdapter.getAllSessionInfo().isEmpty());
+ // TODO: Find a correct session info
for (RoutingSessionInfo sessionInfo : mMr2ProviderServiceAdapter.getAllSessionInfo()) {
- getInstrumentation().runOnMainSync(() ->
- mMr2ProviderServiceAdapter.onReleaseSession(
- MediaRoute2ProviderService.REQUEST_ID_NONE,
- sessionInfo.getId()));
+ getInstrumentation()
+ .runOnMainSync(
+ () ->
+ mMr2ProviderServiceAdapter.onReleaseSession(
+ MediaRoute2ProviderService.REQUEST_ID_NONE,
+ sessionInfo.getId()));
}
assertTrue(onRouteUnselectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// Make sure the route is enabled
@@ -226,7 +404,6 @@
}
}
});
-
}
@Test
@@ -258,7 +435,7 @@
StubMediaRouteProviderService.ROUTE_ID1,
/* sessionHints= */ null));
StubMediaRouteProviderService.StubMediaRouteProvider.StubRouteController createdController =
- mProvider.mControllers.get(StubMediaRouteProviderService.ROUTE_ID1);
+ mMr1Provider.mControllers.get(StubMediaRouteProviderService.ROUTE_ID1);
assertNotNull(createdController); // Avoids nullability warning.
assertNull(createdController.mLastSetVolume);
mMr2ProviderServiceAdapter.setRouteVolume(StubMediaRouteProviderService.ROUTE_ID1, 100);
@@ -274,7 +451,7 @@
public void onBinderDied_releaseRoutingSessions() throws Exception {
String descriptorId = StubMediaRouteProviderService.ROUTE_ID1;
- waitForRoutesAdded();
+ waitForRoutesAdded(descriptorId);
assertNotNull(mRoutes);
RouteInfo routeToSelect = mRoutes.get(descriptorId);
@@ -283,26 +460,27 @@
getInstrumentation().runOnMainSync(() -> mRouter.selectRoute(routeToSelect));
// Wait for a session being created.
- PollingCheck.waitFor(TIMEOUT_MS,
- () -> !mMr2ProviderServiceAdapter.getAllSessionInfo().isEmpty());
+ PollingCheck.waitFor(
+ TIMEOUT_MS, () -> !mMr2ProviderServiceAdapter.getAllSessionInfo().isEmpty());
try {
List<Messenger> messengers =
mServiceImpl.mClients.stream()
.map(client -> client.mMessenger)
.collect(Collectors.toList());
- getInstrumentation().runOnMainSync(() ->
- messengers.forEach(mServiceImpl::onBinderDied));
+ getInstrumentation()
+ .runOnMainSync(() -> messengers.forEach(mServiceImpl::onBinderDied));
// It should have no session info.
- PollingCheck.waitFor(TIMEOUT_MS,
- () -> mMr2ProviderServiceAdapter.getAllSessionInfo().isEmpty());
+ PollingCheck.waitFor(
+ TIMEOUT_MS, () -> mMr2ProviderServiceAdapter.getAllSessionInfo().isEmpty());
} finally {
// Rebind for future tests
- getInstrumentation().runOnMainSync(
- () -> {
- MediaRouter.sGlobal.mRegisteredProviderWatcher.stop();
- MediaRouter.sGlobal.mRegisteredProviderWatcher.start();
- });
+ getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ MediaRouter.sGlobal.mRegisteredProviderWatcher.stop();
+ MediaRouter.sGlobal.mRegisteredProviderWatcher.start();
+ });
}
}
@@ -312,21 +490,24 @@
CountDownLatch onRouterParamsChangedLatch = new CountDownLatch(1);
final MediaRouterParams[] routerParams = {null};
- addCallback(new MediaRouter.Callback() {
- @Override
- public void onRouterParamsChanged(
- @NonNull MediaRouter router, MediaRouterParams params) {
- routerParams[0] = params;
- onRouterParamsChangedLatch.countDown();
- }
- });
+ addCallback(
+ new MediaRouter.Callback() {
+ @Override
+ public void onRouterParamsChanged(
+ @NonNull MediaRouter router, MediaRouterParams params) {
+ routerParams[0] = params;
+ onRouterParamsChangedLatch.countDown();
+ }
+ });
Bundle extras = new Bundle();
extras.putString("test-key", "test-value");
MediaRouterParams params = new MediaRouterParams.Builder().setExtras(extras).build();
- getInstrumentation().runOnMainSync(() -> {
- mRouter.setRouterParams(params);
- });
+ getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mRouter.setRouterParams(params);
+ });
assertTrue(onRouterParamsChangedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
Bundle actualExtras = routerParams[0].getExtras();
@@ -335,29 +516,116 @@
}
void addCallback(MediaRouter.Callback callback) {
- getInstrumentation().runOnMainSync(() -> {
- mRouter.addCallback(mSelector, callback,
- MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY
- | MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
- });
+ getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ mRouter.addCallback(
+ mSelector,
+ callback,
+ MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY
+ | MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ });
mCallbacks.add(callback);
}
- void waitForRoutesAdded() throws Exception {
+ void waitForRoutesAdded(String descriptorId) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
- MediaRouter.Callback callback = new MediaRouter.Callback() {
- @Override
- public void onRouteAdded(@NonNull MediaRouter router, @NonNull RouteInfo route) {
- if (!route.isDefaultOrBluetooth()) {
- latch.countDown();
- }
- }
- };
+ MediaRouter.Callback callback =
+ new MediaRouter.Callback() {
+ @Override
+ public void onRouteAdded(
+ @NonNull MediaRouter router, @NonNull RouteInfo route) {
+ if (!route.isDefaultOrBluetooth()) {
+ MediaRouteDescriptor routeDescriptor = route.getMediaRouteDescriptor();
+ if (routeDescriptor != null
+ && TextUtils.equals(routeDescriptor.getId(), descriptorId)) {
+ latch.countDown();
+ }
+ }
+ }
+ };
addCallback(callback);
latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
- getInstrumentation().runOnMainSync(() -> mRoutes = mRouter.getRoutes().stream().collect(
- Collectors.toMap(route -> route.getDescriptorId(), route -> route)));
+ getInstrumentation()
+ .runOnMainSync(
+ () ->
+ mRoutes =
+ mRouter.getRoutes().stream()
+ .collect(
+ Collectors.toMap(
+ route -> route.getDescriptorId(),
+ route -> route)));
+ }
+
+ void waitForRouteSelected(
+ String descriptorIdToSelect, String selectedDescriptorId, boolean routeSelected)
+ throws Exception {
+ CountDownLatch onRouteSelectedLatch = new CountDownLatch(1);
+ MediaRouter.Callback callback =
+ new MediaRouter.Callback() {
+ @Override
+ public void onRouteSelected(
+ @NonNull MediaRouter router,
+ @NonNull RouteInfo selectedRoute,
+ int reason,
+ @NonNull RouteInfo requestedRoute) {
+ Log.i(
+ TAG,
+ "onRouteSelected with selectedRoute = "
+ + selectedRoute
+ + ", requestedRoute = "
+ + requestedRoute
+ + ", reason = "
+ + reason);
+ if (TextUtils.equals(selectedRoute.getDescriptorId(), selectedDescriptorId)
+ && reason == MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
+ onRouteSelectedLatch.countDown();
+ }
+ }
+ };
+ addCallback(callback);
+
+ RouteInfo routeToSelect = mRoutes.get(descriptorIdToSelect);
+ assertNotNull(routeToSelect);
+
+ getInstrumentation().runOnMainSync(() -> mRouter.selectRoute(routeToSelect));
+ assertEquals(routeSelected, onRouteSelectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ void waitForRouteUnselected(String descriptorIdToUnselect, String deselectedDescriptorId)
+ throws Exception {
+ CountDownLatch onRouteUnselectedLatch = new CountDownLatch(1);
+ MediaRouter.Callback callback =
+ new MediaRouter.Callback() {
+ @Override
+ public void onRouteUnselected(
+ @NonNull MediaRouter router, @NonNull RouteInfo route, int reason) {
+ Log.i(
+ TAG,
+ "onRouteUnselected with route = " + route + ", reason = " + reason);
+ if (TextUtils.equals(route.getDescriptorId(), deselectedDescriptorId)
+ && reason == MediaRouter.UNSELECT_REASON_STOPPED) {
+ onRouteUnselectedLatch.countDown();
+ }
+ }
+ };
+ addCallback(callback);
+
+ RouteInfo routeToUnselect = mRoutes.get(descriptorIdToUnselect);
+ assertNotNull(routeToUnselect);
+
+ getInstrumentation()
+ .runOnMainSync(() -> mRouter.unselect(MediaRouter.UNSELECT_REASON_STOPPED));
+ assertTrue(onRouteUnselectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ private String getMediaRoute2DescriptorId(String descriptorId) {
+ return StubMediaRoute2ProviderService.CLIENT_PACKAGE_NAME
+ + "/"
+ + StubMediaRoute2ProviderService.CLIENT_CLASS_NAME
+ + ":"
+ + descriptorId;
}
}
diff --git a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubMediaRoute2ProviderService.java b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubMediaRoute2ProviderService.java
index 01026cd..5177e23 100644
--- a/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubMediaRoute2ProviderService.java
+++ b/mediarouter/mediarouter/src/androidTest/java/androidx/mediarouter/media/StubMediaRoute2ProviderService.java
@@ -17,19 +17,251 @@
package androidx.mediarouter.media;
import android.content.Context;
+import android.content.IntentFilter;
+import android.util.Log;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.collection.ArrayMap;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
public class StubMediaRoute2ProviderService extends MediaRouteProviderService {
+ private static final Object sLock = new Object();
+
+ public static final String CLIENT_PACKAGE_NAME = "androidx.mediarouter.test";
+ public static final String CLIENT_CLASS_NAME =
+ "androidx.mediarouter.media.StubMediaRoute2ProviderService";
+ public static final String CATEGORY_TEST = "androidx.mediarouter.media.CATEGORY_TEST";
+
+ public static final String ROUTE_ID_GROUP = "route_id_group";
+ public static final String ROUTE_NAME_GROUP = "Group route name";
+ public static final int VOLUME_INITIAL_VALUE = 8;
+ public static final int VOLUME_MAX = 20;
+ public static final String MR2_ROUTE_ID1 = "media_route2_id1";
+ public static final String MR2_ROUTE_NAME1 = "MR2 Sample Route 1";
+ public static final String MR2_ROUTE_ID2 = "media_route2_id2";
+ public static final String MR2_ROUTE_NAME2 = "MR2 Sample Route 2";
+
+ @GuardedBy("sLock")
+ private static StubMediaRoute2ProviderService sInstance;
+
+ private static final List<IntentFilter> CONTROL_FILTERS_TEST;
+
+ static {
+ IntentFilter f1 = new IntentFilter();
+ f1.addCategory(CATEGORY_TEST);
+
+ CONTROL_FILTERS_TEST = new ArrayList<>();
+ CONTROL_FILTERS_TEST.add(f1);
+ }
+
+ /** Gets the instance of StubMediaRoute2ProviderService. */
+ public static StubMediaRoute2ProviderService getInstance() {
+ synchronized (sLock) {
+ return sInstance;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ synchronized (sLock) {
+ sInstance = this;
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ synchronized (sLock) {
+ if (sInstance == this) {
+ sInstance = null;
+ }
+ }
+ }
+
@Override
public MediaRouteProvider onCreateMediaRouteProvider() {
return new StubMediaRoute2Provider(this);
}
class StubMediaRoute2Provider extends MediaRouteProvider {
+ Map<String, MediaRouteDescriptor> mRoutes = new ArrayMap<>();
+ Map<String, List<StubDynamicGroupRouteController>> mDescriptorIdToControllers =
+ new ArrayMap<>();
+ private final MediaRouteDescriptor mGroupDescriptor;
+ boolean mSupportsDynamicGroup = true;
StubMediaRoute2Provider(@NonNull Context context) {
super(context);
+ mGroupDescriptor =
+ new MediaRouteDescriptor.Builder(ROUTE_ID_GROUP, ROUTE_NAME_GROUP)
+ .addControlFilters(CONTROL_FILTERS_TEST)
+ .setVolumeMax(VOLUME_MAX)
+ .setVolume(VOLUME_INITIAL_VALUE)
+ .build();
+ }
+
+ @Override
+ public DynamicGroupRouteController onCreateDynamicGroupRouteController(
+ @NonNull String initialMemberRouteId,
+ @NonNull RouteControllerOptions routeControllerOptions) {
+ Log.i(
+ TAG,
+ "onCreateDynamicGroupRouteController with initialMemberRouteId = "
+ + initialMemberRouteId);
+ StubDynamicGroupRouteController newDynamicRouteController =
+ new StubDynamicGroupRouteController(
+ initialMemberRouteId, routeControllerOptions);
+ addController(initialMemberRouteId, newDynamicRouteController);
+
+ return newDynamicRouteController;
+ }
+
+ public void initializeRoutes() {
+ MediaRouteDescriptor route1 =
+ new MediaRouteDescriptor.Builder(MR2_ROUTE_ID1, MR2_ROUTE_NAME1)
+ .addControlFilters(CONTROL_FILTERS_TEST)
+ .build();
+ MediaRouteDescriptor route2 =
+ new MediaRouteDescriptor.Builder(MR2_ROUTE_ID2, MR2_ROUTE_NAME2)
+ .addControlFilters(CONTROL_FILTERS_TEST)
+ .build();
+ mRoutes.put(route1.getId(), route1);
+ mRoutes.put(route2.getId(), route2);
+ }
+
+ public void publishRoutes() {
+ setDescriptor(
+ new MediaRouteProviderDescriptor.Builder()
+ .addRoutes(mRoutes.values())
+ .setSupportsDynamicGroupRoute(mSupportsDynamicGroup)
+ .build());
+ }
+
+ public void addController(String routeId, StubDynamicGroupRouteController controller) {
+ Log.i(TAG, "addController with routeId = " + routeId + ", controller = " + controller);
+ List<StubDynamicGroupRouteController> controllers =
+ mDescriptorIdToControllers.get(routeId);
+ if (controllers == null) {
+ controllers = new ArrayList<>();
+ }
+ controllers.add(controller);
+ mDescriptorIdToControllers.put(routeId, controllers);
+ }
+
+ public void removeController(String routeId, StubDynamicGroupRouteController controller) {
+ Log.i(
+ TAG,
+ "removeController with routeId = " + routeId + ", controller = " + controller);
+ List<StubDynamicGroupRouteController> controllers =
+ mDescriptorIdToControllers.get(routeId);
+ if (controllers == null) {
+ return;
+ }
+ if (controllers.contains(controller)) {
+ controllers.remove(controller);
+ if (controllers.isEmpty()) {
+ mDescriptorIdToControllers.remove(routeId);
+ } else {
+ mDescriptorIdToControllers.put(routeId, controllers);
+ }
+ }
+ }
+
+ public int getNumberOfCreatedControllers(String descriptorId) {
+ List<StubDynamicGroupRouteController> controllers =
+ mDescriptorIdToControllers.get(descriptorId);
+ return (controllers != null) ? controllers.size() : 0;
+ }
+
+ class StubDynamicGroupRouteController extends DynamicGroupRouteController {
+ final String mRouteId;
+ final RouteControllerOptions mRouteControllerOptions;
+ private final Set<String> mCurrentSelectedRouteIds = new HashSet<>();
+
+ StubDynamicGroupRouteController(
+ String routeId, RouteControllerOptions routeControllerOptions) {
+ mRouteId = routeId;
+ mRouteControllerOptions = routeControllerOptions;
+ mCurrentSelectedRouteIds.add(routeId);
+ }
+
+ private void publishState() {
+ Collection<DynamicRouteDescriptor> dynamicRoutes = buildDynamicRouteDescriptors();
+ Log.i(
+ TAG,
+ "StubDynamicGroupRouteController.publishState() with dynamicRoutes.size() ="
+ + " "
+ + dynamicRoutes.size());
+ notifyDynamicRoutesChanged(mGroupDescriptor, dynamicRoutes);
+ }
+
+ private Collection<DynamicGroupRouteController.DynamicRouteDescriptor>
+ buildDynamicRouteDescriptors() {
+ ArrayList<DynamicGroupRouteController.DynamicRouteDescriptor> result =
+ new ArrayList<>();
+ for (MediaRouteDescriptor route : mRoutes.values()) {
+ DynamicGroupRouteController.DynamicRouteDescriptor dynamicDescriptor =
+ new DynamicGroupRouteController.DynamicRouteDescriptor.Builder(route)
+ .setSelectionState(
+ mCurrentSelectedRouteIds.contains(route.getId())
+ ? DynamicGroupRouteController
+ .DynamicRouteDescriptor.SELECTED
+ : DynamicGroupRouteController
+ .DynamicRouteDescriptor.UNSELECTED)
+ .setIsUnselectable(
+ mCurrentSelectedRouteIds.contains(route.getId()))
+ .build();
+ result.add(dynamicDescriptor);
+ }
+ return result;
+ }
+
+ @Override
+ public void onSelect() {
+ Log.i(TAG, "StubDynamicGroupRouteController.onSelect() with routeId = " + mRouteId);
+ publishState();
+ }
+
+ @Override
+ public void onRelease() {
+ Log.i(
+ TAG,
+ "StubDynamicGroupRouteController.onRelease() with routeId = " + mRouteId);
+ removeController(mRouteId, this);
+ }
+
+ @Override
+ public void onUpdateMemberRoutes(@Nullable List<String> routeIds) {
+ if (routeIds == null) {
+ return;
+ }
+ Log.i(TAG, "StubDynamicGroupRouteController.onUpdateMemberRoutes()");
+ }
+
+ @Override
+ public void onAddMemberRoute(@NonNull String routeId) {
+ Log.i(
+ TAG,
+ "StubDynamicGroupRouteController.onAddMemberRoute with routeId = "
+ + routeId);
+ }
+
+ @Override
+ public void onRemoveMemberRoute(@NonNull String routeId) {
+ Log.i(
+ TAG,
+ "StubDynamicGroupRouteController.onRemoveMemberRoute with routeId = "
+ + routeId);
+ }
}
}
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
index 30d44ee..e5d6b5c 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/GlobalMediaRouter.java
@@ -101,7 +101,7 @@
@VisibleForTesting
RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher;
- MediaRouter.RouteInfo mSelectedRoute;
+ @Nullable MediaRouter.RouteInfo mSelectedRoute;
MediaRouteProvider.RouteController mSelectedRouteController;
MediaRouter.OnPrepareTransferListener mOnPrepareTransferListener;
MediaRouter.PrepareTransferNotifier mTransferNotifier;
@@ -376,7 +376,7 @@
}
@NonNull
- /* package */ MediaRouter.RouteInfo getSelectedRoute() {
+ /* package */ MediaRouter.RouteInfo getSelectedRoute() {
if (mSelectedRoute == null) {
// This should never happen once the media router has been fully
// initialized but it is good to check for the error in case there
@@ -600,7 +600,10 @@
Log.w(TAG, "Ignoring attempt to select disabled route: " + route);
return;
}
-
+ if (isRouteSelected(route)) {
+ Log.w(TAG, "Ignoring attempt to select selected route: " + route);
+ return;
+ }
// Check whether the route comes from MediaRouter2. The SDK check is required to avoid a
// lint error but is not needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
@@ -669,6 +672,25 @@
}
}
+ private boolean isRouteSelected(MediaRouter.RouteInfo route) {
+ if (mSelectedRoute == route) {
+ return true;
+ }
+ MediaRouter.GroupRouteInfo selectedGroupRoute =
+ (mSelectedRoute != null) ? mSelectedRoute.asGroup() : null;
+ if (selectedGroupRoute != null
+ && selectedGroupRoute.getSelectedRoutesInGroup().size() == 1) {
+ int selectionState = selectedGroupRoute.getSelectionState(route);
+ return selectionState
+ == MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor
+ .SELECTED
+ || selectionState
+ == MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor
+ .SELECTING;
+ }
+ return false;
+ }
+
private void notifyRouteConnectionFailed(
@NonNull MediaRouter.RouteInfo route, @MediaRouter.DisconnectReason int reason) {
mCallbackHandler.postRouteDisconnectedMessage(route, /* disconnectedRoute= */ null, reason);
@@ -1645,7 +1667,9 @@
// Nothing to do.
Log.d(
TAG,
- "A RouteController unrelated to the selected route is released."
+ "A RouteController unrelated to the selected route ("
+ + mSelectedRouteController
+ + ") is released."
+ " controller="
+ controller);
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
index 6ff3bef..f037f77 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
@@ -82,6 +82,7 @@
private boolean mMediaTransferRestrictedToSelfProviders;
private List<MediaRoute2Info> mRoutes = new ArrayList<>();
private Map<String, String> mRouteIdToOriginalRouteIdMap = new ArrayMap<>();
+ @Nullable private String mPendingTransferRouteId;
@SuppressWarnings({"SyntheticAccessor"})
MediaRoute2Provider(@NonNull Context context, @NonNull Callback callback) {
@@ -173,6 +174,11 @@
Log.w(TAG, "transferTo: Specified route not found. routeId=" + routeId);
return;
}
+ if (TextUtils.equals(mPendingTransferRouteId, routeId)) {
+ Log.w(TAG, "Ignoring attempt to transfer to pending transfer route: " + route);
+ return;
+ }
+ mPendingTransferRouteId = routeId;
mMediaRouter2.transferTo(route);
}
@@ -438,6 +444,7 @@
@Override
public void onTransfer(@NonNull MediaRouter2.RoutingController oldController,
@NonNull MediaRouter2.RoutingController newController) {
+ mPendingTransferRouteId = null;
mControllerMap.remove(oldController);
if (newController == mMediaRouter2.getSystemController()) {
mCallback.onSelectFallbackRoute(UNSELECT_REASON_ROUTE_CHANGED);
@@ -458,11 +465,13 @@
@Override
public void onTransferFailure(@NonNull MediaRoute2Info requestedRoute) {
+ mPendingTransferRouteId = null;
Log.w(TAG, "Transfer failed. requestedRoute=" + requestedRoute);
}
@Override
public void onStop(@NonNull MediaRouter2.RoutingController routingController) {
+ mPendingTransferRouteId = null;
RouteController routeController = mControllerMap.remove(routingController);
if (routeController != null) {
mCallback.onReleaseController(routeController);