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