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