blob: 773aede11f876f18e6e10b55dded97aecf233b09 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.media.cts;
import static android.media.cts.MediaRouter2Test.releaseControllers;
import static android.media.cts.StubMediaRoute2ProviderService.FEATURE_SAMPLE;
import static android.media.cts.StubMediaRoute2ProviderService.FEATURE_SPECIAL;
import static android.media.cts.StubMediaRoute2ProviderService.ROUTE_ID1;
import static android.media.cts.StubMediaRoute2ProviderService.ROUTE_ID2;
import static android.media.cts.StubMediaRoute2ProviderService.ROUTE_ID4_TO_SELECT_AND_DESELECT;
import static android.media.cts.StubMediaRoute2ProviderService.ROUTE_ID5_TO_TRANSFER_TO;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.media.MediaRoute2Info;
import android.media.MediaRoute2ProviderService;
import android.media.MediaRouter2;
import android.media.MediaRouter2.ControllerCallback;
import android.media.MediaRouter2.RouteCallback;
import android.media.MediaRouter2.RoutingController;
import android.media.MediaRouter2.TransferCallback;
import android.media.RouteDiscoveryPreference;
import android.media.RoutingSessionInfo;
import android.media.cts.StubMediaRoute2ProviderService.Proxy;
import android.os.Bundle;
import android.platform.test.annotations.AppModeFull;
import android.platform.test.annotations.LargeTest;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.PollingCheck;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
@AppModeFull(reason = "The system should be able to bind to StubMediaRoute2ProviderService")
@LargeTest
public class MediaRoute2ProviderServiceTest {
private static final String TAG = "MR2ProviderServiceTest";
Context mContext;
private MediaRouter2 mRouter2;
private Executor mExecutor;
private StubMediaRoute2ProviderService mService;
private static final int TIMEOUT_MS = 5000;
private static final String SESSION_ID_1 = "SESSION_ID_1";
private static final String SESSION_ID_2 = "SESSION_ID_2";
// TODO: Merge these TEST_KEY / TEST_VALUE in all files
public static final String TEST_KEY = "test_key";
public static final String TEST_VALUE = "test_value";
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getTargetContext();
mRouter2 = MediaRouter2.getInstance(mContext);
mExecutor = Executors.newSingleThreadExecutor();
new PollingCheck(TIMEOUT_MS) {
@Override
protected boolean check() {
StubMediaRoute2ProviderService service =
StubMediaRoute2ProviderService.getInstance();
if (service != null) {
mService = service;
return true;
}
return false;
}
}.run();
}
@After
public void tearDown() throws Exception {
if (mService != null) {
mService.clear();
mService = null;
}
}
@Test
public void testGetSessionInfoAndGetAllSessionInfo() {
assertEquals(0, mService.getAllSessionInfo().size());
// Add a session
RoutingSessionInfo sessionInfo1 = new RoutingSessionInfo.Builder(
SESSION_ID_1, "" /* clientPackageName */)
.addSelectedRoute(ROUTE_ID1)
.build();
mService.notifySessionCreated(sessionInfo1, MediaRoute2ProviderService.REQUEST_ID_NONE);
assertEquals(1, mService.getAllSessionInfo().size());
assertEquals(sessionInfo1, mService.getAllSessionInfo().get(0));
assertEquals(sessionInfo1, mService.getSessionInfo(SESSION_ID_1));
// Add another session
RoutingSessionInfo sessionInfo2 = new RoutingSessionInfo.Builder(
SESSION_ID_2, "" /* clientPackageName */)
.addSelectedRoute(ROUTE_ID2)
.build();
mService.notifySessionCreated(
sessionInfo2, MediaRoute2ProviderService.REQUEST_ID_NONE);
assertEquals(2, mService.getAllSessionInfo().size());
assertEquals(sessionInfo2, mService.getSessionInfo(SESSION_ID_2));
// Remove the first session
mService.notifySessionReleased(SESSION_ID_1);
assertNull(mService.getSessionInfo(SESSION_ID_1));
assertEquals(1, mService.getAllSessionInfo().size());
assertEquals(sessionInfo2, mService.getAllSessionInfo().get(0));
assertEquals(sessionInfo2, mService.getSessionInfo(SESSION_ID_2));
// Remove the remaining session
mService.notifySessionReleased(SESSION_ID_2);
assertEquals(0, mService.getAllSessionInfo().size());
assertNull(mService.getSessionInfo(SESSION_ID_2));
}
@Test
public void testNotifyRoutesInvokesMediaRouter2RouteCallback() throws Exception {
final String routeId0 = "routeId0";
final String routeName0 = "routeName0";
final String routeId1 = "routeId1";
final String routeName1 = "routeName1";
final List<String> features = Collections.singletonList("customFeature");
final List<MediaRoute2Info> routes = new ArrayList<>();
routes.add(new MediaRoute2Info.Builder(routeId0, routeName0)
.addFeatures(features)
.build());
routes.add(new MediaRoute2Info.Builder(routeId1, routeName1)
.addFeatures(features)
.build());
final int newConnectionState = MediaRoute2Info.CONNECTION_STATE_CONNECTED;
CountDownLatch onRoutesAddedLatch = new CountDownLatch(1);
CountDownLatch onRoutesChangedLatch = new CountDownLatch(1);
CountDownLatch onRoutesRemovedLatch = new CountDownLatch(1);
RouteCallback routeCallback = new RouteCallback() {
@Override
public void onRoutesAdded(List<MediaRoute2Info> routes) {
if (!features.equals(routes.get(0).getFeatures())) {
return;
}
assertEquals(2, routes.size());
MediaRoute2Info route0;
MediaRoute2Info route1;
if (routeId0.equals(routes.get(0).getOriginalId())) {
route0 = routes.get(0);
route1 = routes.get(1);
} else {
route0 = routes.get(1);
route1 = routes.get(0);
}
assertNotNull(route0);
assertEquals(routeId0, route0.getOriginalId());
assertEquals(routeName0, route0.getName());
assertEquals(features, route0.getFeatures());
assertNotNull(route1);
assertEquals(routeId1, route1.getOriginalId());
assertEquals(routeName1, route1.getName());
assertEquals(features, route1.getFeatures());
onRoutesAddedLatch.countDown();
}
@Override
public void onRoutesChanged(List<MediaRoute2Info> routes) {
if (!features.equals(routes.get(0).getFeatures())) {
return;
}
assertEquals(1, routes.size());
assertEquals(routeId1, routes.get(0).getOriginalId());
assertEquals(newConnectionState, routes.get(0).getConnectionState());
onRoutesChangedLatch.countDown();
}
@Override
public void onRoutesRemoved(List<MediaRoute2Info> routes) {
if (!features.equals(routes.get(0).getFeatures())) {
return;
}
assertEquals(2, routes.size());
MediaRoute2Info route0;
MediaRoute2Info route1;
if (routeId0.equals(routes.get(0).getOriginalId())) {
route0 = routes.get(0);
route1 = routes.get(1);
} else {
route0 = routes.get(1);
route1 = routes.get(0);
}
assertNotNull(route0);
assertEquals(routeId0, route0.getOriginalId());
assertEquals(routeName0, route0.getName());
assertEquals(features, route0.getFeatures());
assertNotNull(route1);
assertEquals(routeId1, route1.getOriginalId());
assertEquals(routeName1, route1.getName());
assertEquals(features, route1.getFeatures());
onRoutesRemovedLatch.countDown();
}
};
mRouter2.registerRouteCallback(mExecutor, routeCallback,
new RouteDiscoveryPreference.Builder(features, true).build());
try {
mService.notifyRoutes(routes);
assertTrue(onRoutesAddedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// Change the connection state of route2 in order to invoke onRoutesChanged()
MediaRoute2Info newRoute2 = new MediaRoute2Info.Builder(routes.get(1))
.setConnectionState(newConnectionState)
.build();
routes.set(1, newRoute2);
mService.notifyRoutes(routes);
assertTrue(onRoutesChangedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
// Now remove all the routes
routes.clear();
mService.notifyRoutes(routes);
assertTrue(onRoutesRemovedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} finally {
mRouter2.unregisterRouteCallback(routeCallback);
}
}
@Test
public void testSessionRelatedCallbacks() throws Exception {
mService.initializeRoutes();
mService.publishRoutes();
List<String> featuresSample = Collections.singletonList(FEATURE_SAMPLE);
Map<String, MediaRoute2Info> routes = waitAndGetRoutes(featuresSample);
MediaRoute2Info routeToCreateSession = routes.get(ROUTE_ID1);
assertNotNull(routeToCreateSession);
Bundle sessionHints = new Bundle();
sessionHints.putString(TEST_KEY, TEST_VALUE);
mRouter2.setOnGetControllerHintsListener(route -> sessionHints);
CountDownLatch onCreateSessionLatch = new CountDownLatch(1);
CountDownLatch onReleaseSessionLatch = new CountDownLatch(1);
CountDownLatch onSelectRouteLatch = new CountDownLatch(1);
CountDownLatch onDeselectRouteLatch = new CountDownLatch(1);
CountDownLatch onTransferToRouteLatch = new CountDownLatch(1);
// Now test all session-related callbacks.
setProxy(new Proxy() {
@Override
public void onCreateSession(long requestId, String packageName, String routeId,
Bundle sessionHints) {
assertEquals(mContext.getPackageName(), packageName);
assertEquals(ROUTE_ID1, routeId);
assertNotNull(sessionHints);
assertTrue(sessionHints.containsKey(TEST_KEY));
assertEquals(TEST_VALUE, sessionHints.getString(TEST_KEY));
RoutingSessionInfo info = new RoutingSessionInfo.Builder(
SESSION_ID_1, mContext.getPackageName())
.addSelectedRoute(ROUTE_ID1)
.addSelectableRoute(ROUTE_ID4_TO_SELECT_AND_DESELECT)
.addTransferableRoute(ROUTE_ID5_TO_TRANSFER_TO)
.build();
mService.notifySessionCreated(info, requestId);
onCreateSessionLatch.countDown();
}
@Override
public void onSelectRoute(long requestId, String sessionId, String routeId) {
assertEquals(SESSION_ID_1, sessionId);
assertEquals(ROUTE_ID4_TO_SELECT_AND_DESELECT, routeId);
RoutingSessionInfo oldInfo = mService.getSessionInfo(SESSION_ID_1);
RoutingSessionInfo newInfo = new RoutingSessionInfo.Builder(oldInfo)
.addSelectedRoute(ROUTE_ID4_TO_SELECT_AND_DESELECT)
.removeSelectableRoute(ROUTE_ID4_TO_SELECT_AND_DESELECT)
.addDeselectableRoute(ROUTE_ID4_TO_SELECT_AND_DESELECT)
.build();
mService.notifySessionUpdated(newInfo);
onSelectRouteLatch.countDown();
}
@Override
public void onDeselectRoute(long requestId, String sessionId, String routeId) {
assertEquals(SESSION_ID_1, sessionId);
assertEquals(ROUTE_ID4_TO_SELECT_AND_DESELECT, routeId);
RoutingSessionInfo oldInfo = mService.getSessionInfo(SESSION_ID_1);
RoutingSessionInfo newInfo = new RoutingSessionInfo.Builder(oldInfo)
.removeSelectedRoute(ROUTE_ID4_TO_SELECT_AND_DESELECT)
.addSelectableRoute(ROUTE_ID4_TO_SELECT_AND_DESELECT)
.removeDeselectableRoute(ROUTE_ID4_TO_SELECT_AND_DESELECT)
.build();
mService.notifySessionUpdated(newInfo);
onDeselectRouteLatch.countDown();
}
@Override
public void onTransferToRoute(long requestId, String sessionId, String routeId) {
assertEquals(SESSION_ID_1, sessionId);
assertEquals(ROUTE_ID5_TO_TRANSFER_TO, routeId);
RoutingSessionInfo oldInfo = mService.getSessionInfo(SESSION_ID_1);
RoutingSessionInfo newInfo = new RoutingSessionInfo.Builder(oldInfo)
.clearDeselectableRoutes()
.clearSelectedRoutes()
.clearDeselectableRoutes()
.addSelectedRoute(ROUTE_ID5_TO_TRANSFER_TO)
.build();
mService.notifySessionUpdated(newInfo);
onTransferToRouteLatch.countDown();
}
@Override
public void onReleaseSession(long requestId, String sessionId) {
assertEquals(SESSION_ID_1, sessionId);
mService.notifySessionReleased(sessionId);
onReleaseSessionLatch.countDown();
}
});
CountDownLatch onControllerCreatedLatch = new CountDownLatch(1);
CountDownLatch onControllerUpdatedForSelectLatch = new CountDownLatch(1);
CountDownLatch onControllerUpdatedForDeselectLatch = new CountDownLatch(1);
CountDownLatch onControllerUpdatedForTransferLatch = new CountDownLatch(1);
List<RoutingController> controllers = new ArrayList<>();
TransferCallback transferCallback = new TransferCallback() {
@Override
public void onTransferred(RoutingController oldController,
RoutingController newController) {
if (newController != null && SESSION_ID_1.equals(newController.getOriginalId())) {
controllers.add(newController);
onControllerCreatedLatch.countDown();
}
}
};
ControllerCallback controllerCallback = new ControllerCallback() {
@Override
public void onControllerUpdated(RoutingController controller) {
List<MediaRoute2Info> selectedRoutes = controller.getSelectedRoutes();
if (onControllerUpdatedForSelectLatch.getCount() > 0) {
if (selectedRoutes.size() == 2
&& ROUTE_ID4_TO_SELECT_AND_DESELECT.equals(
selectedRoutes.get(1).getOriginalId())) {
onControllerUpdatedForSelectLatch.countDown();
}
} else if (onControllerUpdatedForDeselectLatch.getCount() > 0) {
if (selectedRoutes.size() == 1
&& ROUTE_ID1.equals(selectedRoutes.get(0).getOriginalId())) {
onControllerUpdatedForDeselectLatch.countDown();
}
} else if (onControllerUpdatedForTransferLatch.getCount() > 0) {
if (selectedRoutes.size() == 1
&& ROUTE_ID5_TO_TRANSFER_TO.equals(
selectedRoutes.get(0).getOriginalId())) {
onControllerUpdatedForTransferLatch.countDown();
}
}
}
};
// TODO: Remove this once the MediaRouter2 becomes always connected to the mService.
RouteCallback dummyCallback = new RouteCallback();
try {
mRouter2.registerRouteCallback(mExecutor, dummyCallback,
new RouteDiscoveryPreference.Builder(new ArrayList<>(), true).build());
mRouter2.registerTransferCallback(mExecutor, transferCallback);
mRouter2.registerControllerCallback(mExecutor, controllerCallback);
mRouter2.transferTo(routeToCreateSession);
assertTrue(onCreateSessionLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(onControllerCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertFalse(controllers.isEmpty());
RoutingController controller = controllers.get(0);
controller.selectRoute(routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT));
assertTrue(onSelectRouteLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(onControllerUpdatedForSelectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
controller.deselectRoute(routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT));
assertTrue(onDeselectRouteLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(onControllerUpdatedForDeselectLatch.await(
TIMEOUT_MS, TimeUnit.MILLISECONDS));
mRouter2.transferTo(routes.get(ROUTE_ID5_TO_TRANSFER_TO));
assertTrue(onTransferToRouteLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(onControllerUpdatedForTransferLatch.await(
TIMEOUT_MS, TimeUnit.MILLISECONDS));
controller.release();
assertTrue(onReleaseSessionLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} finally {
mRouter2.unregisterRouteCallback(dummyCallback);
mRouter2.unregisterTransferCallback(transferCallback);
mRouter2.unregisterControllerCallback(controllerCallback);
mRouter2.setOnGetControllerHintsListener(null);
releaseControllers(mRouter2.getControllers());
}
}
@Test
public void testNotifySessionReleased() throws Exception {
mService.initializeRoutes();
mService.publishRoutes();
List<String> featuresSample = Collections.singletonList(FEATURE_SAMPLE);
Map<String, MediaRoute2Info> routes = waitAndGetRoutes(featuresSample);
MediaRoute2Info routeToCreateSession = routes.get(ROUTE_ID1);
assertNotNull(routeToCreateSession);
CountDownLatch onCreateSessionLatch = new CountDownLatch(1);
setProxy(new Proxy() {
@Override
public void onCreateSession(long requestId, String packageName, String routeId,
Bundle sessionHints) {
assertEquals(mContext.getPackageName(), packageName);
assertEquals(ROUTE_ID1, routeId);
assertNull(sessionHints);
RoutingSessionInfo info = new RoutingSessionInfo.Builder(
SESSION_ID_1, mContext.getPackageName())
.addSelectedRoute(ROUTE_ID1)
.addSelectableRoute(ROUTE_ID4_TO_SELECT_AND_DESELECT)
.addTransferableRoute(ROUTE_ID5_TO_TRANSFER_TO)
.build();
mService.notifySessionCreated(info, requestId);
onCreateSessionLatch.countDown();
}
});
CountDownLatch onControllerCreatedLatch = new CountDownLatch(1);
CountDownLatch onControllerReleasedLatch = new CountDownLatch(1);
List<RoutingController> controllers = new ArrayList<>();
TransferCallback transferCallback = new TransferCallback() {
@Override
public void onTransferred(RoutingController oldController,
RoutingController newController) {
if (newController != null) {
if (SESSION_ID_1.equals(newController.getOriginalId())) {
controllers.add(newController);
onControllerCreatedLatch.countDown();
}
} else {
// newController == null means that the oldController is released
if (SESSION_ID_1.equals(oldController.getOriginalId())) {
assertTrue(oldController.isReleased());
onControllerReleasedLatch.countDown();
}
}
}
};
// TODO: Remove this once the MediaRouter2 becomes always connected to the mService.
RouteCallback dummyCallback = new RouteCallback();
try {
mRouter2.registerRouteCallback(mExecutor, dummyCallback,
new RouteDiscoveryPreference.Builder(new ArrayList<>(), true).build());
mRouter2.registerTransferCallback(mExecutor, transferCallback);
mRouter2.transferTo(routeToCreateSession);
assertTrue(onCreateSessionLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertTrue(onControllerCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertFalse(controllers.isEmpty());
mService.notifySessionReleased(SESSION_ID_1);
assertTrue(onControllerReleasedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} finally {
mRouter2.unregisterRouteCallback(dummyCallback);
mRouter2.unregisterTransferCallback(transferCallback);
releaseControllers(mRouter2.getControllers());
}
}
@Test
public void testOnDiscoveryPreferenceChanged() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);
RouteCallback routeCallback = new RouteCallback();
RouteCallback routeCallback2 = new RouteCallback();
List<String> featuresSample = Collections.singletonList(FEATURE_SAMPLE);
List<String> featuresSpecial = Collections.singletonList(FEATURE_SPECIAL);
setProxy(new Proxy() {
@Override
public void onDiscoveryPreferenceChanged(RouteDiscoveryPreference preference) {
List<String> features = preference.getPreferredFeatures();
if (features.contains(FEATURE_SAMPLE) && features.contains(FEATURE_SPECIAL)
&& preference.shouldPerformActiveScan()) {
latch.countDown();
}
if (latch.getCount() == 0 && !features.contains(FEATURE_SAMPLE)
&& features.contains(FEATURE_SPECIAL)) {
latch2.countDown();
}
}
});
mRouter2.registerRouteCallback(mExecutor, routeCallback,
new RouteDiscoveryPreference.Builder(featuresSample, true).build());
mRouter2.registerRouteCallback(mExecutor, routeCallback2,
new RouteDiscoveryPreference.Builder(featuresSpecial, true).build());
try {
assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
mRouter2.unregisterRouteCallback(routeCallback);
assertTrue(latch2.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} finally {
mRouter2.unregisterRouteCallback(routeCallback2);
}
}
void setProxy(StubMediaRoute2ProviderService.Proxy proxy) {
StubMediaRoute2ProviderService service = mService;
if (service != null) {
service.setProxy(proxy);
}
}
Map<String, MediaRoute2Info> waitAndGetRoutes(List<String> features)
throws Exception {
CountDownLatch latch = new CountDownLatch(1);
RouteCallback routeCallback = new RouteCallback() {
@Override
public void onRoutesAdded(List<MediaRoute2Info> routes) {
for (MediaRoute2Info route : routes) {
if (!route.isSystemRoute()) {
latch.countDown();
}
}
}
};
mRouter2.registerRouteCallback(mExecutor, routeCallback,
new RouteDiscoveryPreference.Builder(features, true).build());
try {
latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
return createRouteMap(mRouter2.getRoutes());
} finally {
mRouter2.unregisterRouteCallback(routeCallback);
}
}
// Helper for getting routes easily. Uses original ID as a key
static Map<String, MediaRoute2Info> createRouteMap(List<MediaRoute2Info> routes) {
Map<String, MediaRoute2Info> routeMap = new HashMap<>();
for (MediaRoute2Info route : routes) {
routeMap.put(route.getOriginalId(), route);
}
return routeMap;
}
}