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