• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 android.media.router.cts;
18 
19 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
20 import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO;
21 import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
22 import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.app.ActivityManager;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.media.MediaRoute2Info;
32 import android.media.MediaRoute2ProviderService;
33 import android.media.MediaRouter2;
34 import android.media.RouteDiscoveryPreference;
35 import android.media.RoutingSessionInfo;
36 import android.os.Bundle;
37 import android.os.IBinder;
38 import android.text.TextUtils;
39 
40 import com.android.compatibility.common.util.PollingCheck;
41 
42 import org.junit.rules.ExternalResource;
43 
44 import java.lang.reflect.Method;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.HashMap;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Objects;
51 import java.util.concurrent.Executors;
52 
53 import javax.annotation.concurrent.GuardedBy;
54 
55 public class StubMediaRoute2ProviderService extends MediaRoute2ProviderService {
56     private static final String TAG = "SampleMR2ProviderSvc";
57     private static final Object sLock = new Object();
58 
59     public static final String ROUTE_ID1 = "route_id1";
60     public static final String ROUTE_NAME1 = "Sample Route 1";
61     public static final String ROUTE_ID2 = "route_id2";
62     public static final String ROUTE_NAME2 = "Sample Route 2";
63     public static final String ROUTE_ID3_SESSION_CREATION_FAILED =
64             "route_id3_session_creation_failed";
65     public static final String ROUTE_NAME3 = "Sample Route 3 - Session creation failed";
66     public static final String ROUTE_ID4_TO_SELECT_AND_DESELECT =
67             "route_id4_to_select_and_deselect";
68     public static final String ROUTE_NAME4 = "Sample Route 4 - Route to select and deselect";
69     public static final String ROUTE_ID5_TO_TRANSFER_TO = "route_id5_to_transfer_to";
70     public static final String ROUTE_NAME5 = "Sample Route 5 - Route to transfer to";
71 
72     public static final String ROUTE_ID_SPECIAL_FEATURE = "route_special_feature";
73     public static final String ROUTE_NAME_SPECIAL_FEATURE = "Special Feature Route";
74 
75     public static final String ROUTE_ID6_REJECT_SET_VOLUME = "route_id6_reject_set_volume";
76     public static final String ROUTE_NAME_6 = "Sample Route 6 - Reject Set Route Volume";
77 
78     public static final String ROUTE_ID7_STATIC_GROUP = "route_id7_static_group";
79     public static final String ROUTE_NAME7 = "Sample Route 7 - Static Group";
80 
81     public static final String ROUTE_ID8_SYSTEM_TYPE = "route_id8_system_type";
82     public static final String ROUTE_NAME8 = "Sample Route 8 - System Type";
83 
84     public static final int INITIAL_VOLUME = 30;
85     public static final int VOLUME_MAX = 100;
86     public static final int SESSION_VOLUME_MAX = 50;
87     public static final int SESSION_VOLUME_INITIAL = 20;
88 
89     public static final String ROUTE_ID_FIXED_VOLUME = "route_fixed_volume";
90     public static final String ROUTE_NAME_FIXED_VOLUME = "Fixed Volume Route";
91     public static final String ROUTE_ID_VARIABLE_VOLUME = "route_variable_volume";
92     public static final String ROUTE_NAME_VARIABLE_VOLUME = "Variable Volume Route";
93 
94     public static final String FEATURE_SAMPLE = "android.media.router.cts.FEATURE_SAMPLE";
95     public static final String FEATURE_SPECIAL = "android.media.router.cts.FEATURE_SPECIAL";
96 
97     public static final List<String> FEATURES_ALL = new ArrayList();
98     public static final List<String> FEATURES_SPECIAL = new ArrayList();
99     public static final List<String> STATIC_GROUP_SELECTED_ROUTES_IDS = new ArrayList<>();
100     public static final List<String> FEATURE_SPECIAL_ROUTE_IDS = new ArrayList<>();
101 
102     static {
103         FEATURES_ALL.add(FEATURE_SAMPLE);
104         FEATURES_ALL.add(FEATURE_SPECIAL);
105         FEATURES_ALL.add(FEATURE_LIVE_AUDIO);
106 
107         FEATURES_SPECIAL.add(FEATURE_SPECIAL);
108 
109         STATIC_GROUP_SELECTED_ROUTES_IDS.add(ROUTE_ID7_STATIC_GROUP);
110         STATIC_GROUP_SELECTED_ROUTES_IDS.add(ROUTE_ID1);
111 
112         FEATURE_SPECIAL_ROUTE_IDS.add(ROUTE_ID_SPECIAL_FEATURE);
113         FEATURE_SPECIAL_ROUTE_IDS.add(ROUTE_ID7_STATIC_GROUP);
114     }
115 
116     Map<String, MediaRoute2Info> mRoutes = new HashMap<>();
117     Map<String, String> mRouteIdToSessionId = new HashMap<>();
118     private int mNextSessionId = 1000;
119 
120     @GuardedBy("sLock")
121     private static StubMediaRoute2ProviderService sInstance;
122     private Proxy mProxy;
123 
initializeRoutes()124     public void initializeRoutes() {
125         MediaRoute2Info route1 = new MediaRoute2Info.Builder(ROUTE_ID1, ROUTE_NAME1)
126                 .addFeature(FEATURE_SAMPLE)
127                 .build();
128         MediaRoute2Info route2 = new MediaRoute2Info.Builder(ROUTE_ID2, ROUTE_NAME2)
129                 .addFeature(FEATURE_SAMPLE)
130                 .build();
131         MediaRoute2Info route3 = new MediaRoute2Info.Builder(
132                 ROUTE_ID3_SESSION_CREATION_FAILED, ROUTE_NAME3)
133                 .addFeature(FEATURE_SAMPLE)
134                 .build();
135         MediaRoute2Info route4 = new MediaRoute2Info.Builder(
136                 ROUTE_ID4_TO_SELECT_AND_DESELECT, ROUTE_NAME4)
137                 .addFeature(FEATURE_SAMPLE)
138                 .build();
139         MediaRoute2Info route5 = new MediaRoute2Info.Builder(
140                 ROUTE_ID5_TO_TRANSFER_TO, ROUTE_NAME5)
141                 .addFeature(FEATURE_SAMPLE)
142                 .build();
143         MediaRoute2Info route6 = new MediaRoute2Info.Builder(
144                 ROUTE_ID6_REJECT_SET_VOLUME, ROUTE_NAME_6)
145                 .setVolumeHandling(PLAYBACK_VOLUME_VARIABLE)
146                 .setVolume(INITIAL_VOLUME)
147                 .setVolumeMax(VOLUME_MAX)
148                 .addFeature(FEATURE_SAMPLE)
149                 .build();
150         MediaRoute2Info route7 =
151                 new MediaRoute2Info.Builder(ROUTE_ID7_STATIC_GROUP, ROUTE_NAME7)
152                         .setVolumeHandling(PLAYBACK_VOLUME_VARIABLE)
153                         .setVolume(INITIAL_VOLUME)
154                         .setVolumeMax(VOLUME_MAX)
155                         .addFeature(FEATURE_SPECIAL)
156                         .build();
157         MediaRoute2Info route8 =
158                 new MediaRoute2Info.Builder(ROUTE_ID8_SYSTEM_TYPE, ROUTE_NAME8)
159                         .setVolumeHandling(PLAYBACK_VOLUME_FIXED)
160                         .setType(MediaRoute2Info.TYPE_BLUETOOTH_A2DP)
161                         .addFeature(FEATURE_SAMPLE)
162                         .build();
163         MediaRoute2Info routeSpecial =
164                 new MediaRoute2Info.Builder(ROUTE_ID_SPECIAL_FEATURE, ROUTE_NAME_SPECIAL_FEATURE)
165                         .addFeature(FEATURE_SAMPLE)
166                         .addFeature(FEATURE_SPECIAL)
167                         .build();
168         MediaRoute2Info fixedVolumeRoute =
169                 new MediaRoute2Info.Builder(ROUTE_ID_FIXED_VOLUME, ROUTE_NAME_FIXED_VOLUME)
170                         .addFeature(FEATURE_SAMPLE)
171                         .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_FIXED)
172                         .build();
173         MediaRoute2Info variableVolumeRoute =
174                 new MediaRoute2Info.Builder(ROUTE_ID_VARIABLE_VOLUME, ROUTE_NAME_VARIABLE_VOLUME)
175                         .addFeature(FEATURE_SAMPLE)
176                         .setVolumeHandling(PLAYBACK_VOLUME_VARIABLE)
177                         .setVolume(INITIAL_VOLUME)
178                         .setVolumeMax(VOLUME_MAX)
179                         .build();
180 
181         mRoutes.put(route1.getId(), route1);
182         mRoutes.put(route2.getId(), route2);
183         mRoutes.put(route3.getId(), route3);
184         mRoutes.put(route4.getId(), route4);
185         mRoutes.put(route5.getId(), route5);
186         mRoutes.put(route6.getId(), route6);
187         mRoutes.put(route7.getId(), route7);
188         mRoutes.put(route8.getId(), route8);
189         mRoutes.put(routeSpecial.getId(), routeSpecial);
190         mRoutes.put(fixedVolumeRoute.getId(), fixedVolumeRoute);
191         mRoutes.put(variableVolumeRoute.getId(), variableVolumeRoute);
192     }
193 
getInstance()194     public static StubMediaRoute2ProviderService getInstance() {
195         synchronized (sLock) {
196             return sInstance;
197         }
198     }
199 
clear()200     public void clear() {
201         mProxy = null;
202         mRoutes.clear();
203         mRouteIdToSessionId.clear();
204         for (RoutingSessionInfo sessionInfo : getAllSessionInfo()) {
205             notifySessionReleased(sessionInfo.getId());
206         }
207     }
208 
setProxy(@ullable Proxy proxy)209     public void setProxy(@Nullable Proxy proxy) {
210         mProxy = proxy;
211     }
212 
213     @Override
onCreate()214     public void onCreate() {
215         super.onCreate();
216         synchronized (sLock) {
217             sInstance = this;
218         }
219     }
220 
221     @Override
onDestroy()222     public void onDestroy() {
223         super.onDestroy();
224         synchronized (sLock) {
225             if (sInstance == this) {
226                 sInstance = null;
227             }
228         }
229     }
230 
231     @Override
onBind(Intent intent)232     public IBinder onBind(Intent intent) {
233         return super.onBind(intent);
234     }
235 
236     @Override
onSetRouteVolume(long requestId, String routeId, int volume)237     public void onSetRouteVolume(long requestId, String routeId, int volume) {
238         MediaRoute2Info route = mRoutes.get(routeId);
239         if (route == null) {
240             return;
241         }
242 
243         if (TextUtils.equals(route.getOriginalId(), ROUTE_ID6_REJECT_SET_VOLUME)) {
244             notifyRequestFailed(requestId, REASON_REJECTED);
245             return;
246         }
247 
248         volume = Math.max(0, Math.min(volume, route.getVolumeMax()));
249         mRoutes.put(routeId, new MediaRoute2Info.Builder(route)
250                 .setVolume(volume)
251                 .build());
252         publishRoutes();
253     }
254 
255     @Override
onSetSessionVolume(long requestId, String sessionId, int volume)256     public void onSetSessionVolume(long requestId, String sessionId, int volume) {
257         RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
258         if (sessionInfo == null) {
259             return;
260         }
261         volume = Math.max(0, Math.min(volume, sessionInfo.getVolumeMax()));
262         RoutingSessionInfo newSessionInfo = new RoutingSessionInfo.Builder(sessionInfo)
263                 .setVolume(volume)
264                 .build();
265         notifySessionUpdated(newSessionInfo);
266     }
267 
268     @Override
onCreateSession(long requestId, String packageName, String routeId, @Nullable Bundle sessionHints)269     public void onCreateSession(long requestId, String packageName, String routeId,
270             @Nullable Bundle sessionHints) {
271         Proxy proxy = mProxy;
272         if (doesProxyOverridesMethod(proxy, "onCreateSession")) {
273             proxy.onCreateSession(requestId, packageName, routeId, sessionHints);
274             return;
275         }
276 
277         MediaRoute2Info route = mRoutes.get(routeId);
278         if (route == null || TextUtils.equals(ROUTE_ID3_SESSION_CREATION_FAILED, routeId)) {
279             notifyRequestFailed(requestId, REASON_UNKNOWN_ERROR);
280             return;
281         }
282         maybeDeselectRoute(routeId, requestId);
283 
284         final String sessionId = String.valueOf(mNextSessionId);
285         mNextSessionId++;
286 
287         mRoutes.put(routeId, new MediaRoute2Info.Builder(route)
288                 .setClientPackageName(packageName)
289                 .build());
290         mRouteIdToSessionId.put(routeId, sessionId);
291 
292         RoutingSessionInfo.Builder sessionInfoBuilder =
293                 new RoutingSessionInfo.Builder(sessionId, packageName)
294                         .addSelectedRoute(routeId)
295                         .addSelectableRoute(ROUTE_ID4_TO_SELECT_AND_DESELECT)
296                         .addTransferableRoute(ROUTE_ID5_TO_TRANSFER_TO)
297                         .setVolumeHandling(PLAYBACK_VOLUME_VARIABLE)
298                         .setVolumeMax(SESSION_VOLUME_MAX)
299                         .setVolume(SESSION_VOLUME_INITIAL)
300                         // Set control hints with given sessionHints
301                         .setControlHints(sessionHints);
302 
303         if (TextUtils.equals(routeId, ROUTE_ID7_STATIC_GROUP)) {
304             // Add group member routes.
305             sessionInfoBuilder.addSelectedRoute(ROUTE_ID1);
306             sessionInfoBuilder.addDeselectableRoute(ROUTE_ID1);
307 
308             // Set client package name for group member routes.
309             mRoutes.put(
310                     ROUTE_ID1,
311                     new MediaRoute2Info.Builder(mRoutes.get(ROUTE_ID1))
312                             .setClientPackageName(packageName)
313                             .build());
314 
315             mRouteIdToSessionId.put(ROUTE_ID1, sessionId);
316         }
317 
318         notifySessionCreated(requestId, sessionInfoBuilder.build());
319         publishRoutes();
320     }
321 
322     @Override
onReleaseSession(long requestId, String sessionId)323     public void onReleaseSession(long requestId, String sessionId) {
324         Proxy proxy = mProxy;
325         if (doesProxyOverridesMethod(proxy, "onReleaseSession")) {
326             proxy.onReleaseSession(requestId, sessionId);
327             return;
328         }
329 
330         RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
331         if (sessionInfo == null) {
332             return;
333         }
334 
335         for (String routeId : sessionInfo.getSelectedRoutes()) {
336             mRouteIdToSessionId.remove(routeId);
337             MediaRoute2Info route = mRoutes.get(routeId);
338             if (route != null) {
339                 mRoutes.put(routeId, new MediaRoute2Info.Builder(route)
340                         .setClientPackageName(null)
341                         .build());
342             }
343         }
344         notifySessionReleased(sessionId);
345         publishRoutes();
346     }
347 
348     @Override
onDiscoveryPreferenceChanged(RouteDiscoveryPreference preference)349     public void onDiscoveryPreferenceChanged(RouteDiscoveryPreference preference) {
350         Proxy proxy = mProxy;
351         if (doesProxyOverridesMethod(proxy, "onDiscoveryPreferenceChanged")) {
352             proxy.onDiscoveryPreferenceChanged(preference);
353             return;
354         }
355 
356         // Just call the empty super method in order to mark the callback as tested.
357         super.onDiscoveryPreferenceChanged(preference);
358     }
359 
360     @Override
onSelectRoute(long requestId, String sessionId, String routeId)361     public void onSelectRoute(long requestId, String sessionId, String routeId) {
362         Proxy proxy = mProxy;
363         if (doesProxyOverridesMethod(proxy, "onSelectRoute")) {
364             proxy.onSelectRoute(requestId, sessionId, routeId);
365             return;
366         }
367 
368         RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
369         MediaRoute2Info route = mRoutes.get(routeId);
370         if (route == null || sessionInfo == null) {
371             return;
372         }
373         maybeDeselectRoute(routeId, requestId);
374 
375         mRoutes.put(routeId, new MediaRoute2Info.Builder(route)
376                 .setClientPackageName(sessionInfo.getClientPackageName())
377                 .build());
378         mRouteIdToSessionId.put(routeId, sessionId);
379         publishRoutes();
380 
381         RoutingSessionInfo newSessionInfo = new RoutingSessionInfo.Builder(sessionInfo)
382                 .addSelectedRoute(routeId)
383                 .removeSelectableRoute(routeId)
384                 .addDeselectableRoute(routeId)
385                 .build();
386         notifySessionUpdated(newSessionInfo);
387     }
388 
389     @Override
onDeselectRoute(long requestId, String sessionId, String routeId)390     public void onDeselectRoute(long requestId, String sessionId, String routeId) {
391         Proxy proxy = mProxy;
392         if (doesProxyOverridesMethod(proxy, "onDeselectRoute")) {
393             proxy.onDeselectRoute(requestId, sessionId, routeId);
394             return;
395         }
396 
397         RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
398         MediaRoute2Info route = mRoutes.get(routeId);
399 
400         if (sessionInfo == null || route == null
401                 || !sessionInfo.getSelectedRoutes().contains(routeId)) {
402             return;
403         }
404 
405         mRouteIdToSessionId.remove(routeId);
406         mRoutes.put(routeId, new MediaRoute2Info.Builder(route)
407                 .setClientPackageName(null)
408                 .build());
409         publishRoutes();
410 
411         if (sessionInfo.getSelectedRoutes().size() == 1) {
412             notifySessionReleased(sessionId);
413             return;
414         }
415 
416         RoutingSessionInfo newSessionInfo = new RoutingSessionInfo.Builder(sessionInfo)
417                 .removeSelectedRoute(routeId)
418                 .addSelectableRoute(routeId)
419                 .removeDeselectableRoute(routeId)
420                 .build();
421         notifySessionUpdated(newSessionInfo);
422     }
423 
424     @Override
onTransferToRoute(long requestId, String sessionId, String routeId)425     public void onTransferToRoute(long requestId, String sessionId, String routeId) {
426         Proxy proxy = mProxy;
427         if (doesProxyOverridesMethod(proxy, "onTransferToRoute")) {
428             proxy.onTransferToRoute(requestId, sessionId, routeId);
429             return;
430         }
431 
432         RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
433         MediaRoute2Info route = mRoutes.get(routeId);
434 
435         if (sessionInfo == null || route == null) {
436             return;
437         }
438 
439         for (String selectedRouteId : sessionInfo.getSelectedRoutes()) {
440             mRouteIdToSessionId.remove(selectedRouteId);
441             MediaRoute2Info selectedRoute = mRoutes.get(selectedRouteId);
442             if (selectedRoute != null) {
443                 mRoutes.put(selectedRouteId, new MediaRoute2Info.Builder(selectedRoute)
444                         .setClientPackageName(null)
445                         .build());
446             }
447         }
448 
449         mRoutes.put(routeId, new MediaRoute2Info.Builder(route)
450                 .setClientPackageName(sessionInfo.getClientPackageName())
451                 .build());
452         mRouteIdToSessionId.put(routeId, sessionId);
453 
454         RoutingSessionInfo newSessionInfo = new RoutingSessionInfo.Builder(sessionInfo)
455                 .clearSelectedRoutes()
456                 .addSelectedRoute(routeId)
457                 .removeDeselectableRoute(routeId)
458                 .removeTransferableRoute(routeId)
459                 .build();
460         notifySessionUpdated(newSessionInfo);
461         publishRoutes();
462     }
463 
464     /**
465      * Adds a route and publishes it. It could replace a route in the provider if
466      * they have the same route id.
467      */
addRoute(@onNull MediaRoute2Info route)468     public void addRoute(@NonNull MediaRoute2Info route) {
469         Objects.requireNonNull(route, "route must not be null");
470         mRoutes.put(route.getOriginalId(), route);
471         publishRoutes();
472     }
473 
474     /**
475      * Removes a route and publishes it.
476      */
removeRoute(@onNull String routeId)477     public void removeRoute(@NonNull String routeId) {
478         Objects.requireNonNull(routeId, "routeId must not be null");
479         MediaRoute2Info route = mRoutes.get(routeId);
480         if (route != null) {
481             mRoutes.remove(routeId);
482             publishRoutes();
483         }
484     }
485 
486     /** Removes routes that don't match a set of ids, and publishes the remaining routes. */
removeAllRoutesExcept(Collection<String> routeIdsToKeep)487     public void removeAllRoutesExcept(Collection<String> routeIdsToKeep) {
488         List<String> routeIds = List.copyOf(mRoutes.keySet());
489         for (String id : routeIds) {
490             if (!routeIdsToKeep.contains(id)) {
491                 mRoutes.remove(id);
492             }
493         }
494         publishRoutes();
495     }
496 
maybeDeselectRoute(String routeId, long requestId)497     void maybeDeselectRoute(String routeId, long requestId) {
498         if (!mRouteIdToSessionId.containsKey(routeId)) {
499             return;
500         }
501 
502         String sessionId = mRouteIdToSessionId.get(routeId);
503         onDeselectRoute(requestId, sessionId, routeId);
504     }
505 
publishRoutes()506     void publishRoutes() {
507         notifyRoutes(new ArrayList<>(mRoutes.values()));
508     }
509 
510     public static class Proxy {
onCreateSession(long requestId, @NonNull String packageName, @NonNull String routeId, @Nullable Bundle sessionHints)511         public void onCreateSession(long requestId, @NonNull String packageName,
512                 @NonNull String routeId, @Nullable Bundle sessionHints) {}
onReleaseSession(long requestId, @NonNull String sessionId)513         public void onReleaseSession(long requestId, @NonNull String sessionId) {}
onSelectRoute(long requestId, @NonNull String sessionId, @NonNull String routeId)514         public void onSelectRoute(long requestId, @NonNull String sessionId,
515                 @NonNull String routeId) {}
onDeselectRoute(long requestId, @NonNull String sessionId, @NonNull String routeId)516         public void onDeselectRoute(long requestId, @NonNull String sessionId,
517                 @NonNull String routeId) {}
onTransferToRoute(long requestId, @NonNull String sessionId, @NonNull String routeId)518         public void onTransferToRoute(long requestId, @NonNull String sessionId,
519                 @NonNull String routeId) {}
onDiscoveryPreferenceChanged(RouteDiscoveryPreference preference)520         public void onDiscoveryPreferenceChanged(RouteDiscoveryPreference preference) {}
521         // TODO: Handle onSetRouteVolume() && onSetSessionVolume()
522     }
523 
doesProxyOverridesMethod(Proxy proxy, String methodName)524     private static boolean doesProxyOverridesMethod(Proxy proxy, String methodName) {
525         if (proxy == null) {
526             return false;
527         }
528         Method[] methods = proxy.getClass().getMethods();
529         if (methods == null) {
530             return false;
531         }
532         for (int i = 0; i < methods.length; i++) {
533             if (methods[i].getName().equals(methodName)) {
534                 // Found method. Check if it overrides
535                 return methods[i].getDeclaringClass() != Proxy.class;
536             }
537         }
538         return false;
539     }
540 
541     // This class can be used as a JUnit @Rule to initialize and get a reference to a running
542     // StubMediaRoute2ProviderService instance that will remain valid until the end of the test, and
543     // be cleaned up afterwards.
544     public static class Setup extends ExternalResource {
545         private static final long TIMEOUT_MS = 5000;
546         private MediaRouter2 mRouter2;
547         private MediaRouter2.RouteCallback mEmptyCallback;
548         private StubMediaRoute2ProviderService mService;
549 
550         /**
551          * This should be called once per test invocation, to setup and get a reference to a
552          * StubMediaRoute2ProviderService.
553          */
setupAndGetService(Context context)554         public StubMediaRoute2ProviderService setupAndGetService(Context context) {
555             assertThat(mService).isNull();
556 
557             // In order to make the system bind to the test service, we need to set a non-empty
558             // discovery preference while the app is in the foreground.
559             ActivityManager.RunningAppProcessInfo appInfo =
560                     new ActivityManager.RunningAppProcessInfo();
561             ActivityManager.getMyMemoryState(appInfo);
562             assertThat(appInfo.importance).isAtMost(IMPORTANCE_VISIBLE);
563 
564             RouteDiscoveryPreference preference =
565                     new RouteDiscoveryPreference.Builder(List.of("unimportant_value"), false)
566                             .build();
567 
568             mRouter2 = MediaRouter2.getInstance(context);
569 
570             // This callback needs to stay registered until the end of the test, to prevent the
571             // provider service from being torn down prematurely.
572             mEmptyCallback = new MediaRouter2.RouteCallback() {};
573             mRouter2.registerRouteCallback(
574                     Executors.newSingleThreadExecutor(), mEmptyCallback, preference);
575 
576             new PollingCheck(TIMEOUT_MS) {
577                 @Override
578                 protected boolean check() {
579                     mService = StubMediaRoute2ProviderService.getInstance();
580                     if (mService != null) {
581                         return true;
582                     }
583                     return false;
584                 }
585             }.run();
586             assertThat(mService).isNotNull();
587             mService.initializeRoutes();
588             mService.publishRoutes();
589             return mService;
590         }
591 
592         @Override
after()593         protected void after() {
594             if (mEmptyCallback != null) {
595                 mRouter2.unregisterRouteCallback(mEmptyCallback);
596             }
597             if (mService != null) {
598                 mService.clear();
599             }
600         }
601     }
602 }
603