1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.mediaroutertest;
18 
19 import static android.media.MediaRoute2Info.FEATURE_REMOTE_PLAYBACK;
20 import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
21 import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE;
22 import static android.media.MediaRoute2ProviderService.REASON_REJECTED;
23 import static android.media.MediaRoute2ProviderService.REQUEST_ID_NONE;
24 
25 import static androidx.test.ext.truth.os.BundleSubject.assertThat;
26 
27 import static com.android.mediaroutertest.StubMediaRoute2ProviderService.FEATURE_SAMPLE;
28 import static com.android.mediaroutertest.StubMediaRoute2ProviderService.FEATURE_SPECIAL;
29 import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID1;
30 import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID2;
31 import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID4_TO_SELECT_AND_DESELECT;
32 import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID5_TO_TRANSFER_TO;
33 import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID6_TO_BE_IGNORED;
34 import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_FIXED_VOLUME;
35 import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_SPECIAL_FEATURE;
36 import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_VARIABLE_VOLUME;
37 import static com.android.mediaroutertest.StubMediaRoute2ProviderService.VOLUME_MAX;
38 
39 import static com.google.common.truth.Truth.assertThat;
40 import static com.google.common.truth.Truth.assertWithMessage;
41 
42 import static org.junit.Assert.assertThrows;
43 
44 import android.Manifest;
45 import android.app.UiAutomation;
46 import android.content.Context;
47 import android.media.MediaRoute2Info;
48 import android.media.MediaRouter2;
49 import android.media.MediaRouter2.RouteCallback;
50 import android.media.MediaRouter2.TransferCallback;
51 import android.media.MediaRouter2Manager;
52 import android.media.MediaRouter2Utils;
53 import android.media.RouteDiscoveryPreference;
54 import android.media.RoutingSessionInfo;
55 import android.os.Bundle;
56 import android.text.TextUtils;
57 
58 import androidx.test.ext.junit.runners.AndroidJUnit4;
59 import androidx.test.filters.LargeTest;
60 import androidx.test.filters.SmallTest;
61 import androidx.test.platform.app.InstrumentationRegistry;
62 
63 import com.android.compatibility.common.util.PollingCheck;
64 
65 import org.junit.After;
66 import org.junit.Before;
67 import org.junit.Test;
68 import org.junit.runner.RunWith;
69 
70 import java.util.ArrayList;
71 import java.util.Collections;
72 import java.util.HashMap;
73 import java.util.List;
74 import java.util.Map;
75 import java.util.Objects;
76 import java.util.concurrent.CountDownLatch;
77 import java.util.concurrent.Executor;
78 import java.util.concurrent.Executors;
79 import java.util.concurrent.TimeUnit;
80 import java.util.function.Predicate;
81 import java.util.stream.Collectors;
82 
83 @RunWith(AndroidJUnit4.class)
84 @SmallTest
85 public class MediaRouter2ManagerTest {
86     private static final String TAG = "MediaRouter2ManagerTest";
87     private static final int WAIT_TIME_MS = 2000;
88     private static final int TIMEOUT_MS = 5000;
89     private static final String TEST_KEY = "test_key";
90     private static final String TEST_VALUE = "test_value";
91     private static final String TEST_ID_UNKNOWN = "id_unknown";
92     private static final String TEST_NAME_UNKNOWN = "unknown";
93 
94     private Context mContext;
95     private UiAutomation mUiAutomation;
96     private MediaRouter2Manager mManager;
97     private MediaRouter2 mRouter2;
98     private Executor mExecutor;
99     private String mPackageName;
100     private StubMediaRoute2ProviderService mService;
101 
102     private final List<MediaRouter2Manager.Callback> mManagerCallbacks = new ArrayList<>();
103     private final List<RouteCallback> mRouteCallbacks = new ArrayList<>();
104     private final List<MediaRouter2.TransferCallback> mTransferCallbacks = new ArrayList<>();
105 
106     public static final List<String> FEATURES_ALL = new ArrayList();
107     public static final List<String> FEATURES_SPECIAL = new ArrayList();
108 
109     static {
110         FEATURES_ALL.add(FEATURE_SAMPLE);
111         FEATURES_ALL.add(FEATURE_SPECIAL);
112 
113         FEATURES_SPECIAL.add(FEATURE_SPECIAL);
114     }
115 
116     @Before
setUp()117     public void setUp() throws Exception {
118         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
119         mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
120         mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.MEDIA_CONTENT_CONTROL,
121                 Manifest.permission.MODIFY_AUDIO_ROUTING);
122         MediaRouter2ManagerTestActivity.startActivity(mContext);
123 
124         mManager = MediaRouter2Manager.getInstance(mContext);
125         mManager.registerScanRequest();
126         mRouter2 = MediaRouter2.getInstance(mContext);
127 
128         // If we need to support thread pool executors, change this to thread pool executor.
129         mExecutor = Executors.newSingleThreadExecutor();
130         mPackageName = mContext.getPackageName();
131 
132         // In order to make the system bind to the test service,
133         // set a non-empty discovery preference while app is in foreground.
134         List<String> features = new ArrayList<>();
135         features.add("A test feature");
136         RouteDiscoveryPreference preference =
137                 new RouteDiscoveryPreference.Builder(features, false).build();
138         mRouter2.registerRouteCallback(mExecutor, new RouteCallback() {}, preference);
139 
140         new PollingCheck(TIMEOUT_MS) {
141             @Override
142             protected boolean check() {
143                 StubMediaRoute2ProviderService service =
144                         StubMediaRoute2ProviderService.getInstance();
145                 if (service != null) {
146                     mService = service;
147                     return true;
148                 }
149                 return false;
150             }
151         }.run();
152     }
153 
154     @After
tearDown()155     public void tearDown() {
156         mManager.unregisterScanRequest();
157 
158         // order matters (callbacks should be cleared at the last)
159         releaseAllSessions();
160         // unregister callbacks
161         clearCallbacks();
162 
163         if (mService != null) {
164             mService.setProxy(null);
165             mService.setSpy(null);
166         }
167 
168         MediaRouter2ManagerTestActivity.finishActivity();
169         mUiAutomation.dropShellPermissionIdentity();
170     }
171 
172     @Test
testOnRoutesUpdated()173     public void testOnRoutesUpdated() throws Exception {
174         final String routeId0 = "routeId0";
175         final String routeName0 = "routeName0";
176         final String routeId1 = "routeId1";
177         final String routeName1 = "routeName1";
178         final List<String> features = Collections.singletonList("customFeature");
179 
180         final int newConnectionState = MediaRoute2Info.CONNECTION_STATE_CONNECTED;
181 
182         final List<MediaRoute2Info> routes = new ArrayList<>();
183         routes.add(new MediaRoute2Info.Builder(routeId0, routeName0).addFeatures(features).build());
184         routes.add(new MediaRoute2Info.Builder(routeId1, routeName1).addFeatures(features).build());
185 
186         CountDownLatch addedLatch = new CountDownLatch(1);
187         CountDownLatch changedLatch = new CountDownLatch(1);
188         CountDownLatch removedLatch = new CountDownLatch(1);
189 
190         addManagerCallback(
191                 new MediaRouter2Manager.Callback() {
192                     @Override
193                     public void onRoutesUpdated() {
194                         if (addedLatch.getCount() == 1
195                                 && checkRoutesMatch(mManager.getAllRoutes(), routes)) {
196                             addedLatch.countDown();
197                         } else if (changedLatch.getCount() == 1
198                                 && checkRoutesMatch(
199                                         mManager.getAllRoutes(), routes.subList(1, 2))) {
200                             changedLatch.countDown();
201                         } else if (removedLatch.getCount() == 1
202                                 && checkRoutesRemoved(mManager.getAllRoutes(), routes)) {
203                             removedLatch.countDown();
204                         }
205                     }
206                 });
207 
208         mService.addRoutes(routes);
209         assertWithMessage("Added routes not found or onRoutesUpdated() never called.")
210                 .that(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
211 
212         MediaRoute2Info newRoute2 =
213                 new MediaRoute2Info.Builder(routes.get(1))
214                         .setConnectionState(newConnectionState)
215                         .build();
216         routes.set(1, newRoute2);
217         mService.addRoute(routes.get(1));
218         assertWithMessage("Modified route not found or onRoutesUpdated() never called.")
219                 .that(changedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
220 
221         List<String> routeIds = new ArrayList<>();
222         routeIds.add(routeId0);
223         routeIds.add(routeId1);
224 
225         mService.removeRoutes(routeIds);
226         assertWithMessage("Removed routes not found or onRoutesUpdated() never called.")
227                 .that(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
228     }
229 
checkRoutesMatch( List<MediaRoute2Info> routesReceived, List<MediaRoute2Info> expectedRoutes)230     private static boolean checkRoutesMatch(
231             List<MediaRoute2Info> routesReceived, List<MediaRoute2Info> expectedRoutes) {
232         for (MediaRoute2Info expectedRoute : expectedRoutes) {
233             MediaRoute2Info matchingRoute =
234                     routesReceived.stream()
235                             .filter(r -> r.getOriginalId().equals(expectedRoute.getOriginalId()))
236                             .findFirst()
237                             .orElse(null);
238 
239             if (matchingRoute == null) {
240                 return false;
241             }
242             assertThat(TextUtils.equals(expectedRoute.getName(), matchingRoute.getName())).isTrue();
243             assertThat(matchingRoute.getFeatures()).isEqualTo(expectedRoute.getFeatures());
244             assertThat(matchingRoute.getConnectionState())
245                     .isEqualTo(expectedRoute.getConnectionState());
246         }
247 
248         return true;
249     }
250 
checkRoutesRemoved( List<MediaRoute2Info> routesReceived, List<MediaRoute2Info> routesRemoved)251     private static boolean checkRoutesRemoved(
252             List<MediaRoute2Info> routesReceived, List<MediaRoute2Info> routesRemoved) {
253         for (MediaRoute2Info removedRoute : routesRemoved) {
254             if (routesReceived.stream()
255                     .anyMatch(r -> r.getOriginalId().equals(removedRoute.getOriginalId()))) {
256                 return false;
257             }
258         }
259         return true;
260     }
261 
262     @Test
testGetRoutes_removedRoute_returnsCorrectRoutes()263     public void testGetRoutes_removedRoute_returnsCorrectRoutes() throws Exception {
264         CountDownLatch addedLatch = new CountDownLatch(1);
265         CountDownLatch removedLatch = new CountDownLatch(1);
266 
267         RouteCallback routeCallback = new RouteCallback() {
268             // Used to ensure the removed route is added.
269             @Override
270             public void onRoutesAdded(List<MediaRoute2Info> routes) {
271                 if (removedLatch.getCount() > 0) {
272                     return;
273                 }
274                 addedLatch.countDown();
275             }
276 
277             @Override
278             public void onRoutesRemoved(List<MediaRoute2Info> routes) {
279                 removedLatch.countDown();
280             }
281         };
282 
283         mRouter2.registerRouteCallback(mExecutor, routeCallback,
284                 new RouteDiscoveryPreference.Builder(FEATURES_ALL, true).build());
285         mRouteCallbacks.add(routeCallback);
286 
287         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
288         MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2);
289         assertThat(routeToRemove).isNotNull();
290 
291         mService.removeRoute(ROUTE_ID2);
292 
293         // Wait until the route is removed.
294         assertThat(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
295 
296         Map<String, MediaRoute2Info> newRoutes = waitAndGetRoutesWithManager(FEATURES_ALL);
297         assertThat(newRoutes.get(ROUTE_ID2)).isNull();
298 
299         // Revert the removal.
300         mService.addRoute(routeToRemove);
301         assertThat(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
302         mRouter2.unregisterRouteCallback(routeCallback);
303     }
304 
305     /**
306      * Tests if we get proper routes for application that has special route feature.
307      */
308     @Test
testRouteFeatures()309     public void testRouteFeatures() throws Exception {
310         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_SPECIAL);
311 
312         int routeCount = 0;
313         for (MediaRoute2Info route : routes.values()) {
314             if (!route.isSystemRoute()) {
315                 routeCount++;
316             }
317         }
318 
319         assertThat(routeCount).isEqualTo(1);
320         assertThat(routes.get(ROUTE_ID_SPECIAL_FEATURE)).isNotNull();
321     }
322 
323     @Test
testNoAllowedPackages_returnsZeroRoutes()324     public void testNoAllowedPackages_returnsZeroRoutes() throws Exception {
325         RouteDiscoveryPreference preference =
326                 new RouteDiscoveryPreference.Builder(FEATURES_ALL, true)
327                         .setAllowedPackages(List.of("random package name"))
328                         .build();
329         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(preference);
330 
331         int remoteRouteCount = 0;
332         for (MediaRoute2Info route : routes.values()) {
333             if (!route.isSystemRoute()) {
334                 remoteRouteCount++;
335             }
336         }
337 
338         assertThat(remoteRouteCount).isEqualTo(0);
339     }
340 
341     @Test
testAllowedPackages()342     public void testAllowedPackages() throws Exception {
343         RouteDiscoveryPreference preference =
344                 new RouteDiscoveryPreference.Builder(FEATURES_ALL, true)
345                         .setAllowedPackages(List.of("com.android.mediaroutertest"))
346                         .build();
347         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(preference);
348 
349         int remoteRouteCount = 0;
350         for (MediaRoute2Info route : routes.values()) {
351             if (!route.isSystemRoute()) {
352                 remoteRouteCount++;
353             }
354         }
355 
356         assertThat(remoteRouteCount).isGreaterThan(0);
357     }
358 
359     /**
360      * Tests if MR2.SessionCallback.onSessionCreated is called
361      * when a route is selected from MR2Manager.
362      */
363     @Test
testRouterOnSessionCreated()364     public void testRouterOnSessionCreated() throws Exception {
365         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
366 
367         CountDownLatch latch = new CountDownLatch(1);
368 
369         addManagerCallback(new MediaRouter2Manager.Callback() {});
370         addRouterCallback(new MediaRouter2.RouteCallback() {});
371         addTransferCallback(new MediaRouter2.TransferCallback() {
372             @Override
373             public void onTransfer(MediaRouter2.RoutingController oldController,
374                     MediaRouter2.RoutingController newController) {
375                 if (newController == null) {
376                     return;
377                 }
378                 if (createRouteMap(newController.getSelectedRoutes()).containsKey(ROUTE_ID1)) {
379                     latch.countDown();
380                 }
381             }
382         });
383 
384         MediaRoute2Info routeToSelect = routes.get(ROUTE_ID1);
385         assertThat(routeToSelect).isNotNull();
386 
387         mManager.transfer(mPackageName, routeToSelect);
388         assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
389         assertThat(mManager.getRemoteSessions()).hasSize(1);
390     }
391 
392     @Test
testGetRoutingSessions()393     public void testGetRoutingSessions() throws Exception {
394         CountDownLatch latch = new CountDownLatch(1);
395 
396         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
397         MediaRoute2Info routeToSelect = routes.get(ROUTE_ID1);
398 
399         addRouterCallback(new RouteCallback() {});
400         addManagerCallback(new MediaRouter2Manager.Callback() {
401             @Override
402             public void onTransferred(RoutingSessionInfo oldSessionInfo,
403                     RoutingSessionInfo newSessionInfo) {
404                 if (TextUtils.equals(mPackageName, newSessionInfo.getClientPackageName())
405                         && newSessionInfo.getSelectedRoutes().contains(routeToSelect.getId())) {
406                     latch.countDown();
407                 }
408             }
409         });
410 
411         assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(1);
412 
413         mManager.transfer(mPackageName, routeToSelect);
414         assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
415 
416         List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName);
417         assertThat(sessions).hasSize(2);
418 
419         RoutingSessionInfo sessionInfo = sessions.get(1);
420         awaitOnRouteChangedManager(
421                 () -> mManager.releaseSession(sessionInfo),
422                 ROUTE_ID1,
423                 route -> TextUtils.equals(route.getClientPackageName(), null));
424         assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(1);
425     }
426 
427     @Test
testTransfer_unknownRoute_fail()428     public void testTransfer_unknownRoute_fail() throws Exception {
429         addRouterCallback(new RouteCallback() {});
430 
431         CountDownLatch onSessionCreatedLatch = new CountDownLatch(1);
432         CountDownLatch onTransferFailedLatch = new CountDownLatch(1);
433 
434         addManagerCallback(new MediaRouter2Manager.Callback() {
435             @Override
436             public void onTransferred(RoutingSessionInfo oldSessionInfo,
437                     RoutingSessionInfo newSessionInfo) {
438                 assertThat(newSessionInfo).isNotNull();
439                 onSessionCreatedLatch.countDown();
440             }
441             @Override
442             public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) {
443                 onTransferFailedLatch.countDown();
444             }
445         });
446 
447         MediaRoute2Info unknownRoute =
448                 new MediaRoute2Info.Builder(TEST_ID_UNKNOWN, TEST_NAME_UNKNOWN)
449                 .addFeature(FEATURE_REMOTE_PLAYBACK)
450                 .build();
451 
452         mManager.transfer(mManager.getSystemRoutingSession(null), unknownRoute);
453         assertThat(onSessionCreatedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)).isFalse();
454         assertThat(onTransferFailedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
455     }
456 
457     @Test
testRouterRelease_managerGetRoutingSessions()458     public void testRouterRelease_managerGetRoutingSessions() throws Exception {
459         CountDownLatch transferLatch = new CountDownLatch(1);
460         CountDownLatch releaseLatch = new CountDownLatch(1);
461 
462         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
463         MediaRoute2Info routeToSelect = routes.get(ROUTE_ID1);
464         assertThat(routeToSelect).isNotNull();
465 
466         addRouterCallback(new RouteCallback() {});
467         addManagerCallback(new MediaRouter2Manager.Callback() {
468             @Override
469             public void onTransferred(RoutingSessionInfo oldSessionInfo,
470                     RoutingSessionInfo newSessionInfo) {
471                 if (TextUtils.equals(mPackageName, newSessionInfo.getClientPackageName())
472                         && newSessionInfo.getSelectedRoutes().contains(routeToSelect.getId())) {
473                     transferLatch.countDown();
474                 }
475             }
476             @Override
477             public void onSessionReleased(RoutingSessionInfo session) {
478                 releaseLatch.countDown();
479             }
480         });
481 
482         assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(1);
483         assertThat(mRouter2.getControllers()).hasSize(1);
484 
485         mManager.transfer(mManager.getRoutingSessions(mPackageName).get(0), routeToSelect);
486         assertThat(transferLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
487 
488         assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(2);
489         assertThat(mRouter2.getControllers()).hasSize(2);
490         mRouter2.getControllers().get(1).release();
491 
492         assertThat(releaseLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
493 
494         assertThat(mRouter2.getControllers()).hasSize(1);
495         assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(1);
496     }
497 
498     /**
499      * Tests select, transfer, release of routes of a provider
500      */
501     @Test
testSelectAndTransferAndRelease()502     public void testSelectAndTransferAndRelease() throws Exception {
503         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
504         addRouterCallback(new RouteCallback() {});
505 
506         CountDownLatch onSessionCreatedLatch = new CountDownLatch(1);
507 
508         addManagerCallback(new MediaRouter2Manager.Callback() {
509             @Override
510             public void onTransferred(RoutingSessionInfo oldSessionInfo,
511                     RoutingSessionInfo newSessionInfo) {
512                 assertThat(newSessionInfo).isNotNull();
513                 onSessionCreatedLatch.countDown();
514             }
515         });
516         awaitOnRouteChangedManager(
517                 () -> mManager.transfer(mPackageName, routes.get(ROUTE_ID1)),
518                 ROUTE_ID1,
519                 route -> TextUtils.equals(route.getClientPackageName(), mPackageName));
520         assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
521 
522         List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName);
523 
524         assertThat(sessions.size()).isEqualTo(2);
525         RoutingSessionInfo sessionInfo = sessions.get(1);
526 
527         awaitOnRouteChangedManager(
528                 () -> mManager.transfer(mPackageName, routes.get(ROUTE_ID5_TO_TRANSFER_TO)),
529                 ROUTE_ID5_TO_TRANSFER_TO,
530                 route -> TextUtils.equals(route.getClientPackageName(), mPackageName));
531 
532         awaitOnRouteChangedManager(
533                 () -> mManager.releaseSession(sessionInfo),
534                 ROUTE_ID5_TO_TRANSFER_TO,
535                 route -> TextUtils.equals(route.getClientPackageName(), null));
536     }
537 
538     @Test
539     @LargeTest
testTransferTwice()540     public void testTransferTwice() throws Exception {
541         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
542         addRouterCallback(new RouteCallback() { });
543 
544         CountDownLatch successLatch1 = new CountDownLatch(1);
545         CountDownLatch successLatch2 = new CountDownLatch(1);
546         CountDownLatch failureLatch = new CountDownLatch(1);
547         CountDownLatch managerOnSessionReleasedLatch = new CountDownLatch(1);
548         CountDownLatch serviceOnReleaseSessionLatch = new CountDownLatch(1);
549         List<RoutingSessionInfo> sessions = new ArrayList<>();
550 
551         mService.setSpy(new StubMediaRoute2ProviderService.Spy() {
552             @Override
553             public void onReleaseSession(long requestId, String sessionId) {
554                 serviceOnReleaseSessionLatch.countDown();
555             }
556         });
557 
558         addManagerCallback(new MediaRouter2Manager.Callback() {
559             @Override
560             public void onTransferred(RoutingSessionInfo oldSession,
561                     RoutingSessionInfo newSession) {
562                 sessions.add(newSession);
563                 if (successLatch1.getCount() > 0) {
564                     successLatch1.countDown();
565                 } else {
566                     successLatch2.countDown();
567                 }
568             }
569 
570             @Override
571             public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) {
572                 failureLatch.countDown();
573             }
574 
575             @Override
576             public void onSessionReleased(RoutingSessionInfo session) {
577                 managerOnSessionReleasedLatch.countDown();
578             }
579         });
580 
581         MediaRoute2Info route1 = routes.get(ROUTE_ID1);
582         MediaRoute2Info route2 = routes.get(ROUTE_ID2);
583         assertThat(route1).isNotNull();
584         assertThat(route2).isNotNull();
585 
586         mManager.transfer(mPackageName, route1);
587         assertThat(successLatch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
588         mManager.transfer(mPackageName, route2);
589         assertThat(successLatch2.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
590 
591         // onTransferFailed/onSessionReleased should not be called.
592         assertThat(failureLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)).isFalse();
593         assertThat(managerOnSessionReleasedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS))
594                 .isFalse();
595 
596         assertThat(sessions.size()).isEqualTo(2);
597         List<String> remoteSessionIds = mManager.getRemoteSessions().stream()
598                 .map(RoutingSessionInfo::getId)
599                 .collect(Collectors.toList());
600         // The old session shouldn't appear on the session list.
601         assertThat(remoteSessionIds).doesNotContain(sessions.get(0).getId());
602         assertThat(remoteSessionIds).contains(sessions.get(1).getId());
603 
604         assertThat(serviceOnReleaseSessionLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS))
605                 .isFalse();
606         mManager.releaseSession(sessions.get(0));
607         assertThat(serviceOnReleaseSessionLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
608         assertThat(managerOnSessionReleasedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS))
609                 .isFalse();
610     }
611 
612     @Test
613     @LargeTest
testTransfer_ignored_fails()614     public void testTransfer_ignored_fails() throws Exception {
615         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
616         addRouterCallback(new RouteCallback() {});
617 
618         CountDownLatch onSessionCreatedLatch = new CountDownLatch(1);
619         CountDownLatch onFailedLatch = new CountDownLatch(1);
620 
621         addManagerCallback(new MediaRouter2Manager.Callback() {
622             @Override
623             public void onTransferred(RoutingSessionInfo oldSessionInfo,
624                     RoutingSessionInfo newSessionInfo) {
625                 onSessionCreatedLatch.countDown();
626             }
627             @Override
628             public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) {
629                 onFailedLatch.countDown();
630             }
631         });
632 
633         List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName);
634         RoutingSessionInfo targetSession = sessions.get(sessions.size() - 1);
635         mManager.transfer(targetSession, routes.get(ROUTE_ID6_TO_BE_IGNORED));
636 
637         assertThat(onSessionCreatedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)).isFalse();
638         assertThat(onFailedLatch.await(MediaRouter2Manager.TRANSFER_TIMEOUT_MS,
639                 TimeUnit.MILLISECONDS)).isTrue();
640     }
641 
642     @Test
testSetSystemRouteVolume()643     public void testSetSystemRouteVolume() throws Exception {
644         // ensure client
645         addManagerCallback(new MediaRouter2Manager.Callback() {});
646         String selectedSystemRouteId =
647                 MediaRouter2Utils.getOriginalId(
648                 mManager.getSystemRoutingSession(mPackageName).getSelectedRoutes().get(0));
649         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
650         MediaRoute2Info volRoute = routes.get(selectedSystemRouteId);
651         assertThat(volRoute).isNotNull();
652 
653         int originalVolume = volRoute.getVolume();
654         int targetVolume = originalVolume == volRoute.getVolumeMax()
655                 ? originalVolume - 1 : originalVolume + 1;
656 
657         awaitOnRouteChangedManager(
658                 () -> mManager.setRouteVolume(volRoute, targetVolume),
659                 selectedSystemRouteId,
660                 (route -> route.getVolume() == targetVolume));
661 
662         awaitOnRouteChangedManager(
663                 () -> mManager.setRouteVolume(volRoute, originalVolume),
664                 selectedSystemRouteId,
665                 (route -> route.getVolume() == originalVolume));
666     }
667 
668     @Test
testSetRouteVolume()669     public void testSetRouteVolume() throws Exception {
670         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
671         MediaRoute2Info volRoute = routes.get(ROUTE_ID_VARIABLE_VOLUME);
672 
673         int originalVolume = volRoute.getVolume();
674         int targetVolume = originalVolume == volRoute.getVolumeMax()
675                 ? originalVolume - 1 : originalVolume + 1;
676 
677         awaitOnRouteChangedManager(
678                 () -> mManager.setRouteVolume(volRoute, targetVolume),
679                 ROUTE_ID_VARIABLE_VOLUME,
680                 (route -> route.getVolume() == targetVolume));
681 
682         awaitOnRouteChangedManager(
683                 () -> mManager.setRouteVolume(volRoute, originalVolume),
684                 ROUTE_ID_VARIABLE_VOLUME,
685                 (route -> route.getVolume() == originalVolume));
686     }
687 
688     @Test
testSetSessionVolume()689     public void testSetSessionVolume() throws Exception {
690         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
691         addRouterCallback(new RouteCallback() {});
692 
693         CountDownLatch onSessionCreatedLatch = new CountDownLatch(1);
694         CountDownLatch volumeChangedLatch = new CountDownLatch(2);
695 
696         // create a controller
697         addManagerCallback(new MediaRouter2Manager.Callback() {
698             @Override
699             public void onTransferred(RoutingSessionInfo oldSessionInfo,
700                     RoutingSessionInfo newSessionInfo) {
701                 assertThat(newSessionInfo).isNotNull();
702                 onSessionCreatedLatch.countDown();
703             }
704         });
705 
706         mManager.transfer(mPackageName, routes.get(ROUTE_ID1));
707         assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
708 
709         List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName);
710         assertThat(sessions).hasSize(2);
711 
712         // test setSessionVolume
713         RoutingSessionInfo sessionInfo = sessions.get(1);
714         int currentVolume = sessionInfo.getVolume();
715         int targetVolume = (currentVolume == 0) ? 1 : (currentVolume - 1);
716 
717         MediaRouter2.ControllerCallback controllerCallback = new MediaRouter2.ControllerCallback() {
718             @Override
719             public void onControllerUpdated(MediaRouter2.RoutingController controller) {
720                 if (!TextUtils.equals(sessionInfo.getId(), controller.getId())) {
721                     return;
722                 }
723                 if (controller.getVolume() == targetVolume) {
724                     volumeChangedLatch.countDown();
725                 }
726             }
727         };
728 
729         addManagerCallback(new MediaRouter2Manager.Callback() {
730             @Override
731             public void onSessionUpdated(RoutingSessionInfo updatedSessionInfo) {
732                 if (!TextUtils.equals(sessionInfo.getId(), updatedSessionInfo.getId())) {
733                     return;
734                 }
735 
736                 if (updatedSessionInfo.getVolume() == targetVolume) {
737                     volumeChangedLatch.countDown();
738                 }
739             }
740         });
741 
742         try {
743             mRouter2.registerControllerCallback(mExecutor, controllerCallback);
744             mManager.setSessionVolume(sessionInfo, targetVolume);
745             assertThat(volumeChangedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
746         } finally {
747             mRouter2.unregisterControllerCallback(controllerCallback);
748         }
749     }
750 
751     /**
752      * Tests that {@link android.media.MediaRoute2ProviderService#notifyRequestFailed(long, int)}
753      * should invoke the callback only when the right requestId is used.
754      */
755     @Test
testOnRequestFailedCalledForProperRequestId()756     public void testOnRequestFailedCalledForProperRequestId() throws Exception {
757         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
758         MediaRoute2Info volRoute = routes.get(ROUTE_ID_VARIABLE_VOLUME);
759 
760         final List<Long> requestIds = new ArrayList<>();
761         final CountDownLatch onSetRouteVolumeLatch = new CountDownLatch(1);
762         mService.setProxy(new StubMediaRoute2ProviderService.Proxy() {
763             @Override
764             public void onSetRouteVolume(String routeId, int volume, long requestId) {
765                 requestIds.add(requestId);
766                 onSetRouteVolumeLatch.countDown();
767             }
768         });
769 
770         addManagerCallback(new MediaRouter2Manager.Callback() {});
771         mManager.setRouteVolume(volRoute, 0);
772         assertThat(onSetRouteVolumeLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
773         assertThat(requestIds).isNotEmpty();
774 
775         final int failureReason = REASON_REJECTED;
776         final CountDownLatch onRequestFailedLatch = new CountDownLatch(1);
777         final CountDownLatch onRequestFailedSecondCallLatch = new CountDownLatch(1);
778         addManagerCallback(new MediaRouter2Manager.Callback() {
779             @Override
780             public void onRequestFailed(int reason) {
781                 if (reason == failureReason) {
782                     if (onRequestFailedLatch.getCount() > 0) {
783                         onRequestFailedLatch.countDown();
784                     } else {
785                         onRequestFailedSecondCallLatch.countDown();
786                     }
787                 }
788             }
789         });
790 
791         final long invalidRequestId = REQUEST_ID_NONE;
792         mService.notifyRequestFailed(invalidRequestId, failureReason);
793         assertThat(onRequestFailedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)).isFalse();
794 
795         final long validRequestId = requestIds.get(0);
796         mService.notifyRequestFailed(validRequestId, failureReason);
797         assertThat(onRequestFailedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
798 
799         // Test calling notifyRequestFailed() multiple times with the same valid requestId.
800         // onRequestFailed() shouldn't be called since the requestId has been already handled.
801         mService.notifyRequestFailed(validRequestId, failureReason);
802         assertThat(onRequestFailedSecondCallLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS))
803                 .isFalse();
804     }
805 
806     @Test
testVolumeHandling()807     public void testVolumeHandling() throws Exception {
808         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
809 
810         MediaRoute2Info fixedVolumeRoute = routes.get(ROUTE_ID_FIXED_VOLUME);
811         MediaRoute2Info variableVolumeRoute = routes.get(ROUTE_ID_VARIABLE_VOLUME);
812 
813         assertThat(fixedVolumeRoute.getVolumeHandling()).isEqualTo(PLAYBACK_VOLUME_FIXED);
814         assertThat(variableVolumeRoute.getVolumeHandling()).isEqualTo(PLAYBACK_VOLUME_VARIABLE);
815         assertThat(variableVolumeRoute.getVolumeMax()).isEqualTo(VOLUME_MAX);
816     }
817 
818     @Test
testRouter2SetOnGetControllerHintsListener()819     public void testRouter2SetOnGetControllerHintsListener() throws Exception {
820         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
821         addRouterCallback(new RouteCallback() {});
822 
823         MediaRoute2Info route = routes.get(ROUTE_ID1);
824         assertThat(route).isNotNull();
825 
826         final Bundle controllerHints = new Bundle();
827         controllerHints.putString(TEST_KEY, TEST_VALUE);
828         final CountDownLatch hintLatch = new CountDownLatch(1);
829         final MediaRouter2.OnGetControllerHintsListener listener =
830                 route1 -> {
831                     hintLatch.countDown();
832                     return controllerHints;
833                 };
834 
835         final CountDownLatch successLatch = new CountDownLatch(1);
836         final CountDownLatch failureLatch = new CountDownLatch(1);
837 
838         addManagerCallback(new MediaRouter2Manager.Callback() {
839             @Override
840             public void onTransferred(RoutingSessionInfo oldSession,
841                     RoutingSessionInfo newSession) {
842                 assertThat(newSession.getSelectedRoutes()).contains(route.getId());
843                 // The StubMediaRoute2ProviderService is supposed to set control hints
844                 // with the given controllerHints.
845                 Bundle controlHints = newSession.getControlHints();
846                 assertThat(controlHints).isNotNull();
847                 assertThat(controlHints).containsKey(TEST_KEY);
848                 assertThat(controlHints).string(TEST_KEY).isEqualTo(TEST_VALUE);
849 
850                 successLatch.countDown();
851             }
852 
853             @Override
854             public void onTransferFailed(RoutingSessionInfo session,
855                     MediaRoute2Info requestedRoute) {
856                 failureLatch.countDown();
857             }
858         });
859 
860         mRouter2.setOnGetControllerHintsListener(listener);
861         mManager.transfer(mPackageName, route);
862         assertThat(hintLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
863         assertThat(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
864 
865         assertThat(failureLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)).isFalse();
866     }
867 
868     @Test
unregisterScanRequest_enforcesANonNegativeCount()869     public void unregisterScanRequest_enforcesANonNegativeCount() {
870         mManager.unregisterScanRequest(); // One request was made in the test setup.
871         assertThrows(IllegalStateException.class, () -> mManager.unregisterScanRequest());
872         mManager.registerScanRequest(); // So that the cleanup doesn't fail.
873     }
874 
875     /**
876      * Tests if getSelectableRoutes and getDeselectableRoutes filter routes based on
877      * selected routes
878      */
879     @Test
testGetSelectableRoutes_notReturnsSelectedRoutes()880     public void testGetSelectableRoutes_notReturnsSelectedRoutes() throws Exception {
881         Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
882         addRouterCallback(new RouteCallback() {});
883 
884         CountDownLatch onSessionCreatedLatch = new CountDownLatch(1);
885 
886         addManagerCallback(new MediaRouter2Manager.Callback() {
887             @Override
888             public void onTransferred(RoutingSessionInfo oldSessionInfo,
889                     RoutingSessionInfo newSessionInfo) {
890                 assertThat(newSessionInfo).isNotNull();
891                 List<String> selectedRoutes = mManager.getSelectedRoutes(newSessionInfo).stream()
892                         .map(MediaRoute2Info::getId)
893                         .collect(Collectors.toList());
894                 for (MediaRoute2Info selectableRoute :
895                         mManager.getSelectableRoutes(newSessionInfo)) {
896                     assertThat(selectedRoutes).doesNotContain(selectableRoute.getId());
897                 }
898                 for (MediaRoute2Info deselectableRoute :
899                         mManager.getDeselectableRoutes(newSessionInfo)) {
900                     assertThat(selectedRoutes).contains(deselectableRoute.getId());
901                 }
902                 onSessionCreatedLatch.countDown();
903             }
904         });
905 
906         mManager.transfer(mPackageName, routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT));
907         assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
908     }
909 
waitAndGetRoutesWithManager(List<String> routeFeatures)910     Map<String, MediaRoute2Info> waitAndGetRoutesWithManager(List<String> routeFeatures)
911             throws Exception {
912         return waitAndGetRoutesWithManager(
913                 new RouteDiscoveryPreference.Builder(routeFeatures, true).build());
914     }
915 
waitAndGetRoutesWithManager(RouteDiscoveryPreference preference)916     Map<String, MediaRoute2Info> waitAndGetRoutesWithManager(RouteDiscoveryPreference preference)
917             throws Exception {
918         CountDownLatch addedLatch = new CountDownLatch(1);
919         CountDownLatch preferenceLatch = new CountDownLatch(1);
920 
921         // A dummy callback is required to send route feature info.
922         RouteCallback routeCallback = new RouteCallback() {};
923         MediaRouter2Manager.Callback managerCallback =
924                 new MediaRouter2Manager.Callback() {
925                     @Override
926                     public void onRoutesUpdated() {
927                         List<MediaRoute2Info> routes = mManager.getAllRoutes();
928                         for (MediaRoute2Info route : routes) {
929                             if (!route.isSystemRoute()
930                                     && hasMatchingFeature(
931                                             route.getFeatures(),
932                                             preference.getPreferredFeatures())) {
933                                 addedLatch.countDown();
934                                 break;
935                             }
936                         }
937                     }
938 
939                     @Override
940                     public void onDiscoveryPreferenceChanged(
941                             String packageName, RouteDiscoveryPreference discoveryPreference) {
942                         if (TextUtils.equals(mPackageName, packageName)
943                                 && Objects.equals(preference, discoveryPreference)) {
944                             preferenceLatch.countDown();
945                         }
946                     }
947                 };
948         mManager.registerCallback(mExecutor, managerCallback);
949         mRouter2.registerRouteCallback(mExecutor, routeCallback, preference);
950 
951         try {
952             preferenceLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
953             if (mManager.getAvailableRoutes(mPackageName).isEmpty()) {
954                 addedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
955             }
956             return createRouteMap(mManager.getAvailableRoutes(mPackageName));
957         } finally {
958             mRouter2.unregisterRouteCallback(routeCallback);
959             mManager.unregisterCallback(managerCallback);
960         }
961     }
962 
hasMatchingFeature(List<String> features1, List<String> features2)963     boolean hasMatchingFeature(List<String> features1, List<String> features2) {
964         for (String feature : features1) {
965             if (features2.contains(feature)) {
966                 return true;
967             }
968         }
969         return false;
970     }
971 
awaitOnRouteChangedManager(Runnable task, String routeId, Predicate<MediaRoute2Info> predicate)972     void awaitOnRouteChangedManager(Runnable task, String routeId,
973             Predicate<MediaRoute2Info> predicate) throws Exception {
974         CountDownLatch latch = new CountDownLatch(1);
975         MediaRouter2Manager.Callback callback =
976                 new MediaRouter2Manager.Callback() {
977                     @Override
978                     public void onRoutesUpdated() {
979                         MediaRoute2Info route =
980                                 createRouteMap(mManager.getAllRoutes()).get(routeId);
981                         if (route != null && predicate.test(route)) {
982                             latch.countDown();
983                         }
984                     }
985                 };
986         mManager.registerCallback(mExecutor, callback);
987         try {
988             task.run();
989             assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
990         } finally {
991             mManager.unregisterCallback(callback);
992         }
993     }
994 
995     // Helper for getting routes easily
createRouteMap(List<MediaRoute2Info> routes)996     static Map<String, MediaRoute2Info> createRouteMap(List<MediaRoute2Info> routes) {
997         Map<String, MediaRoute2Info> routeMap = new HashMap<>();
998         for (MediaRoute2Info route : routes) {
999             routeMap.put(route.getOriginalId(), route);
1000         }
1001         return routeMap;
1002     }
1003 
addManagerCallback(MediaRouter2Manager.Callback callback)1004     private void addManagerCallback(MediaRouter2Manager.Callback callback) {
1005         mManagerCallbacks.add(callback);
1006         mManager.registerCallback(mExecutor, callback);
1007     }
1008 
addRouterCallback(RouteCallback routeCallback)1009     private void addRouterCallback(RouteCallback routeCallback) {
1010         mRouteCallbacks.add(routeCallback);
1011         mRouter2.registerRouteCallback(mExecutor, routeCallback, RouteDiscoveryPreference.EMPTY);
1012     }
1013 
addTransferCallback(TransferCallback transferCallback)1014     private void addTransferCallback(TransferCallback transferCallback) {
1015         mTransferCallbacks.add(transferCallback);
1016         mRouter2.registerTransferCallback(mExecutor, transferCallback);
1017     }
1018 
clearCallbacks()1019     private void clearCallbacks() {
1020         for (MediaRouter2Manager.Callback callback : mManagerCallbacks) {
1021             mManager.unregisterCallback(callback);
1022         }
1023         mManagerCallbacks.clear();
1024 
1025         for (RouteCallback routeCallback : mRouteCallbacks) {
1026             mRouter2.unregisterRouteCallback(routeCallback);
1027         }
1028         mRouteCallbacks.clear();
1029 
1030         for (MediaRouter2.TransferCallback transferCallback : mTransferCallbacks) {
1031             mRouter2.unregisterTransferCallback(transferCallback);
1032         }
1033         mTransferCallbacks.clear();
1034     }
1035 
releaseAllSessions()1036     private void releaseAllSessions() {
1037         // ensure ManagerRecord in MediaRouter2ServiceImpl
1038         addManagerCallback(new MediaRouter2Manager.Callback() {});
1039 
1040         for (RoutingSessionInfo session : mManager.getRemoteSessions()) {
1041             mManager.releaseSession(session);
1042         }
1043     }
1044 }
1045