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