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