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