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