From 3e241ceb4fe4c1ca2fceb7bc1b5b201d02b56372 Mon Sep 17 00:00:00 2001 From: Hyundo Moon Date: Tue, 9 Mar 2021 16:49:21 +0900 Subject: [PATCH] System MediaRouter2: Add RouteCallback functionality This CL makes the system MediaRouter2 can receive route related events for the client application. Bug: 181122201 Test: Locally tested. Test code will be added when the system APIs are actually makred as @SystemApi. Change-Id: I516d90c725b6a72ba323b30c4c8a013edd737483 --- media/java/android/media/MediaRouter2.java | 211 ++++++++++++++---- .../android/media/MediaRouter2Manager.java | 30 +++ 2 files changed, 201 insertions(+), 40 deletions(-) diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index dc9c58ebf18ce..b4db3055e58c3 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -60,6 +60,7 @@ import java.util.stream.Collectors; public final class MediaRouter2 { private static final String TAG = "MR2"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final Object sSystemRouterLock = new Object(); private static final Object sRouterLock = new Object(); // The maximum time for the old routing controller available after transfer. @@ -67,8 +68,8 @@ public final class MediaRouter2 { // The manager request ID representing that no manager is involved. private static final long MANAGER_REQUEST_ID_NONE = MediaRoute2ProviderService.REQUEST_ID_NONE; - @GuardedBy("sRouterLock") - private static Map sMediaRouter2Map = new ArrayMap<>(); + @GuardedBy("sSystemRouterLock") + private static Map sSystemMediaRouter2Map = new ArrayMap<>(); private static MediaRouter2Manager sManager; @GuardedBy("sRouterLock") @@ -76,6 +77,7 @@ public final class MediaRouter2 { private final Context mContext; private final IMediaRouterService mMediaRouterService; + private final Object mLock = new Object(); private final CopyOnWriteArrayList mRouteCallbackRecords = new CopyOnWriteArrayList<>(); @@ -88,27 +90,29 @@ public final class MediaRouter2 { new CopyOnWriteArrayList<>(); private final String mClientPackageName; + private final ManagerCallback mManagerCallback; + private final String mPackageName; - @GuardedBy("sRouterLock") + @GuardedBy("mLock") final Map mRoutes = new ArrayMap<>(); final RoutingController mSystemController; - @GuardedBy("sRouterLock") + @GuardedBy("mLock") private RouteDiscoveryPreference mDiscoveryPreference = RouteDiscoveryPreference.EMPTY; // TODO: Make MediaRouter2 is always connected to the MediaRouterService. - @GuardedBy("sRouterLock") + @GuardedBy("mLock") MediaRouter2Stub mStub; - @GuardedBy("sRouterLock") + @GuardedBy("mLock") private final Map mNonSystemRoutingControllers = new ArrayMap<>(); private final AtomicInteger mNextRequestId = new AtomicInteger(1); final Handler mHandler; - @GuardedBy("sRouterLock") + @GuardedBy("mLock") private boolean mShouldUpdateRoutes = true; private volatile List mFilteredRoutes = Collections.emptyList(); private volatile OnGetControllerHintsListener mOnGetControllerHintsListener; @@ -130,6 +134,11 @@ public final class MediaRouter2 { /** * Gets an instance of the media router which controls the app's media routing. * Returns {@code null} if the given package name is invalid. + *

+ * Note: For media routers created with this method, the discovery preference passed to + * {@link #registerRouteCallback} will have no effect. The callback will be called accordingly + * with the client app's discovery preference. Therefore, it is recommended to pass + * {@link RouteDiscoveryPreference#EMPTY} there. * * @param clientPackageName the package name of the app to control * @hide @@ -149,15 +158,17 @@ public final class MediaRouter2 { return null; } - synchronized (sRouterLock) { - MediaRouter2 instance = sMediaRouter2Map.get(clientPackageName); + synchronized (sSystemRouterLock) { + MediaRouter2 instance = sSystemMediaRouter2Map.get(clientPackageName); if (instance == null) { // TODO: Add permission check here using MODIFY_AUDIO_ROUTING. if (sManager == null) { sManager = MediaRouter2Manager.getInstance(context.getApplicationContext()); } instance = new MediaRouter2(context, clientPackageName); - sMediaRouter2Map.put(clientPackageName, instance); + sSystemMediaRouter2Map.put(clientPackageName, instance); + // TODO: Remove router instance once it is not needed. + instance.registerManagerCallback(); } return instance; } @@ -192,11 +203,14 @@ public final class MediaRouter2 { } mSystemController = new SystemRoutingController(currentSystemSessionInfo); + // Only used by system MediaRouter2. mClientPackageName = null; + mManagerCallback = null; } private MediaRouter2(Context context, String clientPackageName) { mClientPackageName = clientPackageName; + mManagerCallback = new ManagerCallback(); mContext = context; mMediaRouterService = null; mPackageName = null; @@ -220,8 +234,8 @@ public final class MediaRouter2 { } /** - * Gets the target package name of the app which this media router controls. - * This is only non-null when the router instance is created with the target package name. + * Gets the client package name of the app which this media router controls. + * This is only non-null when the router instance is created with the client package name. * * @see #getInstance(Context, String) * @hide @@ -245,6 +259,9 @@ public final class MediaRouter2 { Objects.requireNonNull(executor, "executor must not be null"); Objects.requireNonNull(routeCallback, "callback must not be null"); Objects.requireNonNull(preference, "preference must not be null"); + if (isSystemRouter()) { + preference = RouteDiscoveryPreference.EMPTY; + } RouteCallbackRecord record = new RouteCallbackRecord(executor, routeCallback, preference); @@ -253,7 +270,11 @@ public final class MediaRouter2 { // is happening but it's okay because either this or the other registration should be done. mRouteCallbackRecords.addIfAbsent(record); - synchronized (sRouterLock) { + if (isSystemRouter()) { + return; + } + + synchronized (mLock) { if (mStub == null) { MediaRouter2Stub stub = new MediaRouter2Stub(); try { @@ -289,7 +310,11 @@ public final class MediaRouter2 { return; } - synchronized (sRouterLock) { + if (isSystemRouter()) { + return; + } + + synchronized (mLock) { if (mStub == null) { return; } @@ -325,23 +350,38 @@ public final class MediaRouter2 { return true; } + /** + * Gets the list of all discovered routes. + * This list includes the routes that are not related to the client app. + *

+ * This will return an empty list for non-system media routers. + * + * @hide + */ + //@SystemApi + public List getAllRoutes() { + if (isSystemRouter()) { + return sManager.getAllRoutes(); + } + return Collections.emptyList(); + } + /** * Gets the unmodifiable list of {@link MediaRoute2Info routes} currently * known to the media router. *

* Please note that the list can be changed before callbacks are invoked. *

- * * @return the list of routes that contains at least one of the route features in discovery * preferences registered by the application */ @NonNull public List getRoutes() { - if (mClientPackageName != null) { + if (isSystemRouter()) { return sManager.getAvailableRoutes(mClientPackageName); } - synchronized (sRouterLock) { + synchronized (mLock) { if (mShouldUpdateRoutes) { mShouldUpdateRoutes = false; @@ -449,7 +489,7 @@ public final class MediaRouter2 { * @see TransferCallback#onTransferFailure */ public void transferTo(@NonNull MediaRoute2Info route) { - if (mClientPackageName != null) { + if (isSystemRouter()) { sManager.selectRoute(mClientPackageName, route); return; } @@ -464,7 +504,7 @@ public final class MediaRouter2 { * controls the media routing, this method is a no-op. */ public void stop() { - if (mClientPackageName != null) { + if (isSystemRouter()) { List sessionInfos = sManager.getRoutingSessions(mClientPackageName); RoutingSessionInfo sessionToRelease = sessionInfos.get(sessionInfos.size() - 1); sManager.releaseSession(sessionToRelease); @@ -481,7 +521,7 @@ public final class MediaRouter2 { */ //@SystemApi public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) { - if (mClientPackageName != null) { + if (isSystemRouter()) { sManager.transfer(controller.getRoutingSessionInfo(), route); return; } @@ -490,7 +530,7 @@ public final class MediaRouter2 { Objects.requireNonNull(route, "route must not be null"); boolean routeFound; - synchronized (sRouterLock) { + synchronized (mLock) { // TODO: Check thread-safety routeFound = mRoutes.containsKey(route.getId()); } @@ -526,7 +566,7 @@ public final class MediaRouter2 { } MediaRouter2Stub stub; - synchronized (sRouterLock) { + synchronized (mLock) { stub = mStub; } if (stub != null) { @@ -576,7 +616,7 @@ public final class MediaRouter2 { public List getControllers() { // TODO: Do not create the controller instances every time, // Instead, update the list using the sessions' ID and session related callbacks. - if (mClientPackageName != null) { + if (isSystemRouter()) { return sManager.getRoutingSessions(mClientPackageName).stream() .map(info -> new RoutingController(info)) .collect(Collectors.toList()); @@ -584,7 +624,7 @@ public final class MediaRouter2 { List result = new ArrayList<>(); result.add(0, mSystemController); - synchronized (sRouterLock) { + synchronized (mLock) { result.addAll(mNonSystemRoutingControllers.values()); } return result; @@ -603,7 +643,7 @@ public final class MediaRouter2 { Objects.requireNonNull(route, "route must not be null"); MediaRouter2Stub stub; - synchronized (sRouterLock) { + synchronized (mLock) { stub = mStub; } if (stub != null) { @@ -627,7 +667,7 @@ public final class MediaRouter2 { List removedRoutes = new ArrayList<>(); List changedRoutes = new ArrayList<>(); - synchronized (sRouterLock) { + synchronized (mLock) { List currentRoutesIds = currentRoutes.stream().map(MediaRoute2Info::getId) .collect(Collectors.toList()); @@ -685,7 +725,7 @@ public final class MediaRouter2 { void addRoutesOnHandler(List routes) { List addedRoutes = new ArrayList<>(); - synchronized (sRouterLock) { + synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { @@ -701,7 +741,7 @@ public final class MediaRouter2 { void removeRoutesOnHandler(List routes) { List removedRoutes = new ArrayList<>(); - synchronized (sRouterLock) { + synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.remove(route.getId()); if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { @@ -717,7 +757,7 @@ public final class MediaRouter2 { void changeRoutesOnHandler(List routes) { List changedRoutes = new ArrayList<>(); - synchronized (sRouterLock) { + synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { @@ -789,7 +829,7 @@ public final class MediaRouter2 { newController.setRoutingSessionInfo(sessionInfo); } else { newController = new RoutingController(sessionInfo); - synchronized (sRouterLock) { + synchronized (mLock) { mNonSystemRoutingControllers.put(newController.getId(), newController); } } @@ -812,7 +852,7 @@ public final class MediaRouter2 { } RoutingController matchingController; - synchronized (sRouterLock) { + synchronized (mLock) { matchingController = mNonSystemRoutingControllers.get(sessionInfo.getId()); } @@ -840,7 +880,7 @@ public final class MediaRouter2 { } RoutingController matchingController; - synchronized (sRouterLock) { + synchronized (mLock) { matchingController = mNonSystemRoutingControllers.get(sessionInfo.getId()); } @@ -868,7 +908,7 @@ public final class MediaRouter2 { if (oldSession.isSystemSession()) { controller = getSystemController(); } else { - synchronized (sRouterLock) { + synchronized (mLock) { controller = mNonSystemRoutingControllers.get(oldSession.getId()); } } @@ -878,6 +918,22 @@ public final class MediaRouter2 { requestCreateController(controller, route, managerRequestId); } + /** + * Returns whether this router is created with {@link #getInstance(Context, String)}. + * This kind of router can control the target app's media routing. + */ + private boolean isSystemRouter() { + return mClientPackageName != null; + } + + /** + * Registers {@link MediaRouter2Manager.Callback} for getting events. + */ + private void registerManagerCallback() { + // Using direct executor here, since MediaRouter2Manager also posts to the main handler. + sManager.registerCallback(Runnable::run, mManagerCallback); + } + private List filterRoutes(List routes, RouteDiscoveryPreference discoveryRequest) { return routes.stream() @@ -1236,7 +1292,7 @@ public final class MediaRouter2 { } MediaRouter2Stub stub; - synchronized (sRouterLock) { + synchronized (mLock) { stub = mStub; } if (stub != null) { @@ -1283,7 +1339,7 @@ public final class MediaRouter2 { } MediaRouter2Stub stub; - synchronized (sRouterLock) { + synchronized (mLock) { stub = mStub; } if (stub != null) { @@ -1318,7 +1374,7 @@ public final class MediaRouter2 { } MediaRouter2Stub stub; - synchronized (sRouterLock) { + synchronized (mLock) { stub = mStub; } if (stub != null) { @@ -1352,7 +1408,7 @@ public final class MediaRouter2 { return; } MediaRouter2Stub stub; - synchronized (sRouterLock) { + synchronized (mLock) { stub = mStub; } if (stub != null) { @@ -1386,7 +1442,7 @@ public final class MediaRouter2 { mState = CONTROLLER_STATE_RELEASING; } - synchronized (sRouterLock) { + synchronized (mLock) { // It could happen if the controller is released by the another thread // in between two locks if (!mNonSystemRoutingControllers.remove(getId(), this)) { @@ -1415,7 +1471,7 @@ public final class MediaRouter2 { mState = CONTROLLER_STATE_RELEASED; } - synchronized (sRouterLock) { + synchronized (mLock) { mNonSystemRoutingControllers.remove(getId(), this); if (shouldReleaseSession && mStub != null) { @@ -1483,7 +1539,7 @@ public final class MediaRouter2 { } private List getRoutesWithIds(List routeIds) { - synchronized (sRouterLock) { + synchronized (mLock) { return routeIds.stream().map(mRoutes::get) .filter(Objects::nonNull) .collect(Collectors.toList()); @@ -1665,4 +1721,79 @@ public final class MediaRouter2 { MediaRouter2.this, oldSession, route, managerRequestId)); } } + + class ManagerCallback implements MediaRouter2Manager.Callback { + + @Override + public void onRoutesAdded(@NonNull List routes) { + List filteredRoutes = + sManager.filterRoutesForPackage(routes, mClientPackageName); + if (filteredRoutes.isEmpty()) { + return; + } + for (RouteCallbackRecord record: mRouteCallbackRecords) { + record.mExecutor.execute( + () -> record.mRouteCallback.onRoutesAdded(filteredRoutes)); + } + } + + @Override + public void onRoutesRemoved(@NonNull List routes) { + List filteredRoutes = + sManager.filterRoutesForPackage(routes, mClientPackageName); + if (filteredRoutes.isEmpty()) { + return; + } + for (RouteCallbackRecord record: mRouteCallbackRecords) { + record.mExecutor.execute( + () -> record.mRouteCallback.onRoutesRemoved(filteredRoutes)); + } + } + + @Override + public void onRoutesChanged(@NonNull List routes) { + List filteredRoutes = + sManager.filterRoutesForPackage(routes, mClientPackageName); + if (filteredRoutes.isEmpty()) { + return; + } + for (RouteCallbackRecord record: mRouteCallbackRecords) { + record.mExecutor.execute( + () -> record.mRouteCallback.onRoutesChanged(filteredRoutes)); + } + } + + @Override + public void onSessionUpdated(@NonNull RoutingSessionInfo session) { + // TODO: Call ControllerCallback.onControllerUpdated + } + + @Override + public void onTransferred(@NonNull RoutingSessionInfo oldSession, + @Nullable RoutingSessionInfo newSession) { + // TODO: Call TransferCallback.onTransfer + } + + @Override + public void onTransferFailed(@NonNull RoutingSessionInfo session, + @NonNull MediaRoute2Info route) { + // TODO: Call TransferCallback.onTransferFailure + } + + @Override + public void onSessionReleased(@NonNull RoutingSessionInfo session) { + // TODO: Call TransferCallback.onStop() + } + + @Override + public void onPreferredFeaturesChanged(@NonNull String packageName, + @NonNull List preferredFeatures) { + // Does nothing. + } + + @Override + public void onRequestFailed(int reason) { + // Does nothing. + } + } } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 5e732f9be68fd..ca619d4072c34 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -236,6 +236,36 @@ public final class MediaRouter2Manager { return routes; } + /** + * Returns a list of routes which are related to the given package name in the given route list. + */ + @NonNull + public List filterRoutesForPackage(@NonNull List routes, + @NonNull String packageName) { + Objects.requireNonNull(routes, "routes must not be null"); + Objects.requireNonNull(packageName, "packageName must not be null"); + + List sessions = getRoutingSessions(packageName); + RoutingSessionInfo sessionInfo = sessions.get(sessions.size() - 1); + + List result = new ArrayList<>(); + List preferredFeatures = mPreferredFeaturesMap.get(packageName); + if (preferredFeatures == null) { + preferredFeatures = Collections.emptyList(); + } + + synchronized (mRoutesLock) { + for (MediaRoute2Info route : routes) { + if (route.hasAnyFeatures(preferredFeatures) + || sessionInfo.getSelectedRoutes().contains(route.getId()) + || sessionInfo.getTransferableRoutes().contains(route.getId())) { + result.add(route); + } + } + } + return result; + } + /** * Gets the system routing session associated with no specific application. */