/* * Copyright 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mediaroutertest; import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE; import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Intent; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderService; import android.media.RoutingSessionInfo; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import javax.annotation.concurrent.GuardedBy; public class StubMediaRoute2ProviderService extends MediaRoute2ProviderService { private static final String TAG = "SampleMR2ProviderSvc"; private static final Object sLock = new Object(); public static final String ROUTE_ID1 = "route_id1"; public static final String ROUTE_NAME1 = "Sample Route 1"; public static final String ROUTE_ID2 = "route_id2"; public static final String ROUTE_NAME2 = "Sample Route 2"; public static final String ROUTE_ID3_SESSION_CREATION_FAILED = "route_id3_session_creation_failed"; public static final String ROUTE_NAME3 = "Sample Route 3 - Session creation failed"; public static final String ROUTE_ID4_TO_SELECT_AND_DESELECT = "route_id4_to_select_and_deselect"; public static final String ROUTE_NAME4 = "Sample Route 4 - Route to select and deselect"; public static final String ROUTE_ID5_TO_TRANSFER_TO = "route_id5_to_transfer_to"; public static final String ROUTE_NAME5 = "Sample Route 5 - Route to transfer to"; public static final String ROUTE_ID6_TO_BE_IGNORED = "route_id6_to_be_ignored"; public static final String ROUTE_NAME6 = "Sample Route 6 - Route to be ignored"; public static final String ROUTE_ID_SPECIAL_FEATURE = "route_special_feature"; public static final String ROUTE_NAME_SPECIAL_FEATURE = "Special Feature Route"; public static final int VOLUME_MAX = 100; public static final int SESSION_VOLUME_MAX = 50; public static final int SESSION_VOLUME_INITIAL = 20; public static final String ROUTE_ID_FIXED_VOLUME = "route_fixed_volume"; public static final String ROUTE_NAME_FIXED_VOLUME = "Fixed Volume Route"; public static final String ROUTE_ID_VARIABLE_VOLUME = "route_variable_volume"; public static final String ROUTE_NAME_VARIABLE_VOLUME = "Variable Volume Route"; public static final String FEATURE_SAMPLE = "com.android.mediaroutertest.FEATURE_SAMPLE"; public static final String FEATURE_SPECIAL = "com.android.mediaroutertest..FEATURE_SPECIAL"; Map mRoutes = new HashMap<>(); Map mRouteIdToSessionId = new HashMap<>(); private int mNextSessionId = 1000; @GuardedBy("sLock") private static StubMediaRoute2ProviderService sInstance; private Proxy mProxy; private Spy mSpy; private void initializeRoutes() { MediaRoute2Info route1 = new MediaRoute2Info.Builder(ROUTE_ID1, ROUTE_NAME1) .addFeature(FEATURE_SAMPLE) .setType(TYPE_REMOTE_TV) .build(); MediaRoute2Info route2 = new MediaRoute2Info.Builder(ROUTE_ID2, ROUTE_NAME2) .addFeature(FEATURE_SAMPLE) .setType(TYPE_REMOTE_SPEAKER) .build(); MediaRoute2Info route3 = new MediaRoute2Info.Builder( ROUTE_ID3_SESSION_CREATION_FAILED, ROUTE_NAME3) .addFeature(FEATURE_SAMPLE) .build(); MediaRoute2Info route4 = new MediaRoute2Info.Builder( ROUTE_ID4_TO_SELECT_AND_DESELECT, ROUTE_NAME4) .addFeature(FEATURE_SAMPLE) .build(); MediaRoute2Info route5 = new MediaRoute2Info.Builder( ROUTE_ID5_TO_TRANSFER_TO, ROUTE_NAME5) .addFeature(FEATURE_SAMPLE) .build(); MediaRoute2Info route6 = new MediaRoute2Info.Builder( ROUTE_ID6_TO_BE_IGNORED, ROUTE_NAME6) .addFeature(FEATURE_SAMPLE) .build(); MediaRoute2Info routeSpecial = new MediaRoute2Info.Builder(ROUTE_ID_SPECIAL_FEATURE, ROUTE_NAME_SPECIAL_FEATURE) .addFeature(FEATURE_SAMPLE) .addFeature(FEATURE_SPECIAL) .build(); MediaRoute2Info fixedVolumeRoute = new MediaRoute2Info.Builder(ROUTE_ID_FIXED_VOLUME, ROUTE_NAME_FIXED_VOLUME) .addFeature(FEATURE_SAMPLE) .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_FIXED) .build(); MediaRoute2Info variableVolumeRoute = new MediaRoute2Info.Builder(ROUTE_ID_VARIABLE_VOLUME, ROUTE_NAME_VARIABLE_VOLUME) .addFeature(FEATURE_SAMPLE) .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) .setVolumeMax(VOLUME_MAX) .build(); mRoutes.put(route1.getId(), route1); mRoutes.put(route2.getId(), route2); mRoutes.put(route3.getId(), route3); mRoutes.put(route4.getId(), route4); mRoutes.put(route5.getId(), route5); mRoutes.put(route6.getId(), route6); mRoutes.put(routeSpecial.getId(), routeSpecial); mRoutes.put(fixedVolumeRoute.getId(), fixedVolumeRoute); mRoutes.put(variableVolumeRoute.getId(), variableVolumeRoute); } public static StubMediaRoute2ProviderService getInstance() { synchronized (sLock) { return sInstance; } } /** * Adds a route and publishes it. It could replace a route in the provider if * they have the same route id. */ public void addRoute(@NonNull MediaRoute2Info route) { addRoutes(Collections.singletonList(route)); } /** * Adds a list of routes and publishes it. It will replace existing routes with matching ids. * * @param routes list of routes to be added. */ public void addRoutes(@NonNull List routes) { Objects.requireNonNull(routes, "Routes must not be null."); for (MediaRoute2Info route : routes) { Objects.requireNonNull(route, "Route must not be null"); mRoutes.put(route.getOriginalId(), route); } publishRoutes(); } /** Removes a route and publishes it. */ public void removeRoute(@NonNull String routeId) { removeRoutes(Collections.singletonList(routeId)); } /** * Removes a list of routes and publishes the changes. * * @param routes list of route ids to be removed. */ public void removeRoutes(@NonNull List routes) { Objects.requireNonNull(routes, "Routes must not be null"); boolean hasRemovedRoutes = false; for (String routeId : routes) { MediaRoute2Info route = mRoutes.get(routeId); if (route != null) { mRoutes.remove(routeId); hasRemovedRoutes = true; } } if (hasRemovedRoutes) { publishRoutes(); } } @Override public void onCreate() { synchronized (sLock) { sInstance = this; } initializeRoutes(); } @Override public void onDestroy() { super.onDestroy(); synchronized (sLock) { if (sInstance == this) { sInstance = null; } } } @Override public IBinder onBind(Intent intent) { publishRoutes(); return super.onBind(intent); } @Override public void onSetRouteVolume(long requestId, String routeId, int volume) { Proxy proxy = mProxy; if (proxy != null) { proxy.onSetRouteVolume(routeId, volume, requestId); return; } MediaRoute2Info route = mRoutes.get(routeId); if (route == null) { return; } volume = Math.max(0, Math.min(volume, route.getVolumeMax())); mRoutes.put(routeId, new MediaRoute2Info.Builder(route) .setVolume(volume) .build()); publishRoutes(); } @Override public void onSetSessionVolume(long requestId, String sessionId, int volume) { RoutingSessionInfo sessionInfo = getSessionInfo(sessionId); if (sessionInfo == null) { return; } volume = Math.max(0, Math.min(volume, sessionInfo.getVolumeMax())); RoutingSessionInfo newSessionInfo = new RoutingSessionInfo.Builder(sessionInfo) .setVolume(volume) .build(); notifySessionUpdated(newSessionInfo); } @Override public void onCreateSession(long requestId, String packageName, String routeId, @Nullable Bundle sessionHints) { MediaRoute2Info route = mRoutes.get(routeId); if (route == null || TextUtils.equals(ROUTE_ID3_SESSION_CREATION_FAILED, routeId)) { notifyRequestFailed(requestId, REASON_UNKNOWN_ERROR); return; } // Ignores the request intentionally for testing if (TextUtils.equals(ROUTE_ID6_TO_BE_IGNORED, routeId)) { return; } maybeDeselectRoute(routeId); final String sessionId = String.valueOf(mNextSessionId); mNextSessionId++; mRoutes.put(routeId, new MediaRoute2Info.Builder(route) .setClientPackageName(packageName) .build()); mRouteIdToSessionId.put(routeId, sessionId); RoutingSessionInfo sessionInfo = new RoutingSessionInfo.Builder(sessionId, packageName) .addSelectedRoute(routeId) .addSelectableRoute(ROUTE_ID4_TO_SELECT_AND_DESELECT) .addTransferableRoute(ROUTE_ID5_TO_TRANSFER_TO) .setVolumeHandling(PLAYBACK_VOLUME_VARIABLE) .setVolumeMax(SESSION_VOLUME_MAX) .setVolume(SESSION_VOLUME_INITIAL) // Set control hints with given sessionHints .setControlHints(sessionHints) .build(); notifySessionCreated(requestId, sessionInfo); publishRoutes(); } @Override public void onReleaseSession(long requestId, String sessionId) { Spy spy = mSpy; if (spy != null) { spy.onReleaseSession(requestId, sessionId); } RoutingSessionInfo sessionInfo = getSessionInfo(sessionId); if (sessionInfo == null) { return; } for (String routeId : sessionInfo.getSelectedRoutes()) { mRouteIdToSessionId.remove(routeId); MediaRoute2Info route = mRoutes.get(routeId); if (route != null) { mRoutes.put(routeId, new MediaRoute2Info.Builder(route) .setClientPackageName(null) .build()); } } notifySessionReleased(sessionId); publishRoutes(); } @Override public void onSelectRoute(long requestId, String sessionId, String routeId) { RoutingSessionInfo sessionInfo = getSessionInfo(sessionId); MediaRoute2Info route = mRoutes.get(routeId); if (route == null || sessionInfo == null) { return; } maybeDeselectRoute(routeId); mRoutes.put(routeId, new MediaRoute2Info.Builder(route) .setClientPackageName(sessionInfo.getClientPackageName()) .build()); mRouteIdToSessionId.put(routeId, sessionId); RoutingSessionInfo newSessionInfo = new RoutingSessionInfo.Builder(sessionInfo) .addSelectedRoute(routeId) .removeSelectableRoute(routeId) .addDeselectableRoute(routeId) .build(); notifySessionUpdated(newSessionInfo); } @Override public void onDeselectRoute(long requestId, String sessionId, String routeId) { RoutingSessionInfo sessionInfo = getSessionInfo(sessionId); MediaRoute2Info route = mRoutes.get(routeId); if (sessionInfo == null || route == null || !sessionInfo.getSelectedRoutes().contains(routeId)) { return; } mRouteIdToSessionId.remove(routeId); mRoutes.put(routeId, new MediaRoute2Info.Builder(route) .setClientPackageName(null) .build()); if (sessionInfo.getSelectedRoutes().size() == 1) { notifySessionReleased(sessionId); return; } RoutingSessionInfo newSessionInfo = new RoutingSessionInfo.Builder(sessionInfo) .removeSelectedRoute(routeId) .addSelectableRoute(routeId) .removeDeselectableRoute(routeId) .build(); notifySessionUpdated(newSessionInfo); } @Override public void onTransferToRoute(long requestId, String sessionId, String routeId) { RoutingSessionInfo sessionInfo = getSessionInfo(sessionId); MediaRoute2Info route = mRoutes.get(routeId); if (sessionInfo == null || route == null) { return; } for (String selectedRouteId : sessionInfo.getSelectedRoutes()) { mRouteIdToSessionId.remove(selectedRouteId); MediaRoute2Info selectedRoute = mRoutes.get(selectedRouteId); if (selectedRoute != null) { mRoutes.put(selectedRouteId, new MediaRoute2Info.Builder(selectedRoute) .setClientPackageName(null) .build()); } } mRoutes.put(routeId, new MediaRoute2Info.Builder(route) .setClientPackageName(sessionInfo.getClientPackageName()) .build()); mRouteIdToSessionId.put(routeId, sessionId); RoutingSessionInfo newSessionInfo = new RoutingSessionInfo.Builder(sessionInfo) .clearSelectedRoutes() .addSelectedRoute(routeId) .removeDeselectableRoute(routeId) .removeTransferableRoute(routeId) .build(); notifySessionUpdated(newSessionInfo); publishRoutes(); } void maybeDeselectRoute(String routeId) { if (!mRouteIdToSessionId.containsKey(routeId)) { return; } String sessionId = mRouteIdToSessionId.get(routeId); onDeselectRoute(REQUEST_ID_NONE, sessionId, routeId); } void publishRoutes() { notifyRoutes(mRoutes.values()); } public void setProxy(@Nullable Proxy proxy) { mProxy = proxy; } public void setSpy(@Nullable Spy spy) { mSpy = spy; } /** * It overrides the original service */ public static class Proxy { public void onSetRouteVolume(String routeId, int volume, long requestId) {} } /** * It gets notified but doesn't prevent the original methods to be called. */ public static class Spy { public void onReleaseSession(long requestId, String sessionId) {} } }