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