Loading core/api/current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -24168,6 +24168,7 @@ package android.media { method @Nullable public android.media.MediaRouter2.RoutingController getController(@NonNull String); method @NonNull public java.util.List<android.media.MediaRouter2.RoutingController> getControllers(); method @NonNull public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context); method @FlaggedApi("com.android.media.flags.enable_cross_user_routing_in_media_router2") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MEDIA_CONTENT_CONTROL, android.Manifest.permission.MEDIA_ROUTING_CONTROL}) public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.os.UserHandle); method @FlaggedApi("com.android.media.flags.enable_rlp_callbacks_in_media_router2") @Nullable public android.media.RouteListingPreference getRouteListingPreference(); method @NonNull public java.util.List<android.media.MediaRoute2Info> getRoutes(); method @NonNull public android.media.MediaRouter2.RoutingController getSystemController(); media/java/android/media/IMediaRouterService.aidl +2 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; import android.os.Bundle; import android.os.UserHandle; /** * {@hide} Loading @@ -50,7 +51,6 @@ interface IMediaRouterService { // MediaRouterService.java for readability. // Methods for MediaRouter2 boolean verifyPackageExists(String clientPackageName); List<MediaRoute2Info> getSystemRoutes(); RoutingSessionInfo getSystemSessionInfo(); Loading @@ -76,6 +76,7 @@ interface IMediaRouterService { List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager); RoutingSessionInfo getSystemSessionInfoForPackage(String packageName); void registerManager(IMediaRouter2Manager manager, String packageName); void registerProxyRouter(IMediaRouter2Manager manager, String callingPackageName, String targetPackageName, in UserHandle targetUser); void unregisterManager(IMediaRouter2Manager manager); void setRouteVolumeWithManager(IMediaRouter2Manager manager, int requestId, in MediaRoute2Info route, int volume); Loading media/java/android/media/MediaRouter2.java +140 −52 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.media; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2; import static com.android.media.flags.Flags.FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2; import android.Manifest; import android.annotation.CallbackExecutor; Loading @@ -32,8 +33,10 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; Loading Loading @@ -78,8 +81,11 @@ 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; private record PackageNameUserHandlePair(String packageName, UserHandle user) {} @GuardedBy("sSystemRouterLock") private static final Map<String, MediaRouter2> sSystemMediaRouter2Map = new ArrayMap<>(); private static final Map<PackageNameUserHandlePair, MediaRouter2> sAppToProxyRouterMap = new ArrayMap<>(); @GuardedBy("sRouterLock") private static MediaRouter2 sInstance; Loading Loading @@ -161,66 +167,121 @@ public final class MediaRouter2 { } /** * Gets an instance of the system media router which controls the app's media routing. Returns * {@code null} if the given package name is invalid. There are several things to note when * using the media routers created with this method. * * <p>First of all, 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. * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app * specified by {@code clientPackageName}. Returns {@code null} if the specified package name * does not exist. * * <p>Also, do not keep/compare the instances of the {@link RoutingController}, since they are * always newly created with the latest session information whenever below methods are called: * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances: * * <ul> * <li>{@link #getControllers()} * <li>{@link #getController(String)} * <li>{@link TransferCallback#onTransfer(RoutingController, RoutingController)} * <li>{@link TransferCallback#onStop(RoutingController)} * <li>{@link ControllerCallback#onControllerUpdated(RoutingController)} * <li> * <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery * preference} passed by a proxy router. Use {@link RouteDiscoveryPreference#EMPTY} when * setting a route callback. * <li> * <p>Methods returning non-system {@link RoutingController controllers} always return * new instances with the latest data. Do not attempt to compare or store them. Instead, * use {@link #getController(String)} or {@link #getControllers()} to query the most * up-to-date state. * <li> * <p>Calls to {@link #setOnGetControllerHintsListener} are ignored. * </ul> * * Therefore, in order to track the current routing status, keep the controller's ID instead, * and use {@link #getController(String)} and {@link #getSystemController()} for getting * controllers. * * <p>Finally, it will have no effect to call {@link #setOnGetControllerHintsListener}. * * @param clientPackageName the package name of the app to control * @throws SecurityException if the caller doesn't have {@link * Manifest.permission#MEDIA_CONTENT_CONTROL MEDIA_CONTENT_CONTROL} permission. * @hide */ // TODO (b/311711420): Deprecate once #getInstance(Context, Looper, String, UserHandle) // reaches public SDK. @SystemApi @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) @Nullable public static MediaRouter2 getInstance( @NonNull Context context, @NonNull String clientPackageName) { Objects.requireNonNull(context, "context must not be null"); Objects.requireNonNull(clientPackageName, "clientPackageName must not be null"); // Note: Even though this check could be somehow bypassed, the other permission checks // in system server will not allow MediaRouter2Manager to be registered. IMediaRouterService serviceBinder = IMediaRouterService.Stub.asInterface( ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); // Capturing the IAE here to not break nullability. try { // verifyPackageExists throws SecurityException if the caller doesn't hold // MEDIA_CONTENT_CONTROL permission. if (!serviceBinder.verifyPackageExists(clientPackageName)) { return findOrCreateProxyInstanceForCallingUser( context, Looper.getMainLooper(), clientPackageName, context.getUser()); } catch (IllegalArgumentException ex) { Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring."); return null; } } catch (RemoteException e) { e.rethrowFromSystemServer(); } /** * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app * specified by {@code clientPackageName} and {@code user}. * * <p>You can specify any {@link Looper} of choice on which internal state updates will run. * * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances: * * <ul> * <li> * <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery * preference} passed by a proxy router. Use a {@link RouteDiscoveryPreference} with empty * {@link RouteDiscoveryPreference.Builder#setPreferredFeatures(List) preferred features} * when setting a route callback. * <li> * <p>Methods returning non-system {@link RoutingController controllers} always return * new instances with the latest data. Do not attempt to compare or store them. Instead, * use {@link #getController(String)} or {@link #getControllers()} to query the most * up-to-date state. * <li> * <p>Calls to {@link #setOnGetControllerHintsListener} are ignored. * </ul> * * @param context The {@link Context} of the caller. * @param looper The {@link Looper} on which to process internal state changes. * @param clientPackageName The package name of the app you want to control the routing of. * @param user The {@link UserHandle} of the user running the app for which to get the proxy * router instance. Must match {@link Process#myUserHandle()} if the caller doesn't hold * {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}. * @throws SecurityException if {@code user} does not match {@link Process#myUserHandle()} and * the caller does not hold {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}. * @throws IllegalArgumentException if {@code clientPackageName} does not exist in {@code user}. */ @FlaggedApi(FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2) @RequiresPermission( anyOf = { Manifest.permission.MEDIA_CONTENT_CONTROL, Manifest.permission.MEDIA_ROUTING_CONTROL }) @NonNull public static MediaRouter2 getInstance( @NonNull Context context, @NonNull Looper looper, @NonNull String clientPackageName, @NonNull UserHandle user) { return findOrCreateProxyInstanceForCallingUser(context, looper, clientPackageName, user); } /** * Returns the per-process singleton proxy router instance for the {@code clientPackageName} and * {@code user} if it exists, or otherwise it creates the appropriate instance. * * <p>If no instance has been created previously, the method will create an instance via {@link * #MediaRouter2(Context, Looper, String, UserHandle)}. */ @NonNull private static MediaRouter2 findOrCreateProxyInstanceForCallingUser( Context context, Looper looper, String clientPackageName, UserHandle user) { Objects.requireNonNull(context, "context must not be null"); Objects.requireNonNull(looper, "looper must not be null"); Objects.requireNonNull(user, "user must not be null"); if (TextUtils.isEmpty(clientPackageName)) { throw new IllegalArgumentException("clientPackageName must not be null or empty"); } PackageNameUserHandlePair key = new PackageNameUserHandlePair(clientPackageName, user); synchronized (sSystemRouterLock) { MediaRouter2 instance = sSystemMediaRouter2Map.get(clientPackageName); MediaRouter2 instance = sAppToProxyRouterMap.get(key); if (instance == null) { instance = new MediaRouter2(context, clientPackageName); sSystemMediaRouter2Map.put(clientPackageName, instance); instance = new MediaRouter2(context, looper, clientPackageName, user); sAppToProxyRouterMap.put(key, instance); } return instance; } Loading Loading @@ -304,9 +365,10 @@ public final class MediaRouter2 { mSystemController = new SystemRoutingController(currentSystemSessionInfo); } private MediaRouter2(Context context, String clientPackageName) { private MediaRouter2( Context context, Looper looper, String clientPackageName, UserHandle user) { mContext = context; mHandler = new Handler(Looper.getMainLooper()); mHandler = new Handler(looper); mMediaRouterService = IMediaRouterService.Stub.asInterface( ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); Loading @@ -315,7 +377,7 @@ public final class MediaRouter2 { new SystemRoutingController( ProxyMediaRouter2Impl.getSystemSessionInfoImpl( mMediaRouterService, clientPackageName)); mImpl = new ProxyMediaRouter2Impl(context, clientPackageName); mImpl = new ProxyMediaRouter2Impl(context, clientPackageName, user); } /** Loading Loading @@ -2000,25 +2062,30 @@ public final class MediaRouter2 { */ private class ProxyMediaRouter2Impl implements MediaRouter2Impl { // Fields originating from MediaRouter2Manager. private final MediaRouter2Manager mManager; private final IMediaRouter2Manager.Stub mClient; private final CopyOnWriteArrayList<MediaRouter2Manager.TransferRequest> mTransferRequests = new CopyOnWriteArrayList<>(); private final AtomicInteger mScanRequestCount = new AtomicInteger(/* initialValue= */ 0); // Fields originating from MediaRouter2. @NonNull private final String mClientPackageName; // TODO(b/281072508): Implement scan request counting when MediaRouter2Manager is removed. @NonNull private final UserHandle mClientUser; private final AtomicBoolean mIsScanning = new AtomicBoolean(/* initialValue= */ false); ProxyMediaRouter2Impl(@NonNull Context context, @NonNull String clientPackageName) { mManager = MediaRouter2Manager.getInstance(context.getApplicationContext()); ProxyMediaRouter2Impl( @NonNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user) { mClientUser = user; mClientPackageName = clientPackageName; mClient = new Client(); try { mMediaRouterService.registerManager( mClient, context.getApplicationContext().getPackageName()); mMediaRouterService.registerProxyRouter( mClient, context.getApplicationContext().getPackageName(), clientPackageName, user); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } Loading @@ -2029,14 +2096,35 @@ public final class MediaRouter2 { @Override public void startScan() { if (!mIsScanning.getAndSet(true)) { mManager.registerScanRequest(); if (mScanRequestCount.getAndIncrement() == 0) { try { mMediaRouterService.startScan(mClient); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } } } @Override public void stopScan() { if (mIsScanning.getAndSet(false)) { mManager.unregisterScanRequest(); if (mScanRequestCount.updateAndGet( count -> { if (count == 0) { throw new IllegalStateException( "No active scan requests to unregister."); } else { return --count; } }) == 0) { try { mMediaRouterService.stopScan(mClient); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } } } Loading media/java/android/media/flags/media_better_together.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -55,3 +55,10 @@ flag { description: "Allow access to privileged routing capabilities to MEDIA_ROUTING_CONTROL holders." bug: "305919655" } flag { name: "enable_cross_user_routing_in_media_router2" namespace: "media_solutions" description: "Allows clients of privileged MediaRouter2 that hold INTERACT_ACROSS_USERS_FULL to control routing across users." bug: "288580225" } services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +113 −38 Original line number Diff line number Diff line Loading @@ -193,26 +193,6 @@ class MediaRouter2ServiceImpl { // Start of methods that implement MediaRouter2 operations. @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) @NonNull public boolean verifyPackageExists(@NonNull String clientPackageName) { final int pid = Binder.getCallingPid(); final int uid = Binder.getCallingUid(); final long token = Binder.clearCallingIdentity(); try { // TODO (b/305919655) - Handle revoking of MEDIA_ROUTING_CONTROL at runtime. enforcePrivilegedRoutingPermissions(uid, pid, /* callerPackageName */ null); PackageManager pm = mContext.getPackageManager(); pm.getPackageInfo(clientPackageName, PackageManager.PackageInfoFlags.of(0)); return true; } catch (PackageManager.NameNotFoundException ex) { return false; } finally { Binder.restoreCallingIdentity(token); } } @NonNull public List<MediaRoute2Info> getSystemRoutes() { final int uid = Binder.getCallingUid(); Loading Loading @@ -491,13 +471,65 @@ class MediaRouter2ServiceImpl { final int callerUid = Binder.getCallingUid(); final int callerPid = Binder.getCallingPid(); final int callerUserId = UserHandle.getUserHandleForUid(callerUid).getIdentifier(); final UserHandle callerUser = Binder.getCallingUserHandle(); // TODO (b/305919655) - Handle revoking of MEDIA_ROUTING_CONTROL at runtime. enforcePrivilegedRoutingPermissions(callerUid, callerPid, callerPackageName); final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { registerManagerLocked( manager, callerUid, callerPid, callerPackageName, /* targetPackageName */ null, callerUser); } } finally { Binder.restoreCallingIdentity(token); } } @RequiresPermission( anyOf = { Manifest.permission.MEDIA_CONTENT_CONTROL, Manifest.permission.MEDIA_ROUTING_CONTROL }) public void registerProxyRouter( @NonNull IMediaRouter2Manager manager, @NonNull String callerPackageName, @NonNull String targetPackageName, @NonNull UserHandle targetUser) { Objects.requireNonNull(manager, "manager must not be null"); Objects.requireNonNull(targetUser, "targetUser must not be null"); if (TextUtils.isEmpty(targetPackageName)) { throw new IllegalArgumentException("targetPackageName must not be empty"); } int callerUid = Binder.getCallingUid(); int callerPid = Binder.getCallingPid(); final long token = Binder.clearCallingIdentity(); try { // TODO (b/305919655) - Handle revoking of MEDIA_ROUTING_CONTROL at runtime. enforcePrivilegedRoutingPermissions(callerUid, callerPid, callerPackageName); enforceCrossUserPermissions(callerUid, callerPid, targetUser); if (!verifyPackageExistsForUser(targetPackageName, targetUser)) { throw new IllegalArgumentException( "targetPackageName does not exist: " + targetPackageName); } synchronized (mLock) { registerManagerLocked( manager, callerUid, callerPid, callerPackageName, callerUserId); manager, callerUid, callerPid, callerPackageName, targetPackageName, targetUser); } } finally { Binder.restoreCallingIdentity(token); Loading Loading @@ -761,6 +793,37 @@ class MediaRouter2ServiceImpl { } } @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS) private boolean verifyPackageExistsForUser( @NonNull String clientPackageName, @NonNull UserHandle user) { try { PackageManager pm = mContext.getPackageManager(); pm.getPackageInfoAsUser( clientPackageName, PackageManager.PackageInfoFlags.of(0), user.getIdentifier()); return true; } catch (PackageManager.NameNotFoundException ex) { return false; } } /** * Enforces the caller has {@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} if the * caller's user is different from the target user. */ private void enforceCrossUserPermissions( int callerUid, int callerPid, @NonNull UserHandle targetUser) { int callerUserId = UserHandle.getUserId(callerUid); if (targetUser.getIdentifier() != callerUserId) { mContext.enforcePermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, callerPid, callerUid, "Must hold INTERACT_ACROSS_USERS_FULL to control an app in a different" + " userId."); } } // End of methods that implements operations for both MediaRouter2 and MediaRouter2Manager. public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { Loading Loading @@ -1203,7 +1266,8 @@ class MediaRouter2ServiceImpl { int callerUid, int callerPid, @NonNull String callerPackageName, int callerUserId) { @Nullable String targetPackageName, @NonNull UserHandle targetUser) { final IBinder binder = manager.asBinder(); ManagerRecord managerRecord = mAllManagerRecords.get(binder); Loading @@ -1217,15 +1281,18 @@ class MediaRouter2ServiceImpl { TAG, TextUtils.formatSimple( "registerManager | callerUid: %d, callerPid: %d, callerPackage: %s," + " callerUserId: %d", callerUid, callerPid, callerPackageName, callerUserId)); // TODO (b/305919655) - Handle revoking of MEDIA_ROUTING_CONTROL at runtime. enforcePrivilegedRoutingPermissions(callerUid, callerPid, callerPackageName); UserRecord userRecord = getOrCreateUserRecordLocked(callerUserId); managerRecord = new ManagerRecord( userRecord, manager, callerUid, callerPid, callerPackageName); + "targetPackageName: %s, targetUserId: %d", callerUid, callerPid, callerPackageName, targetPackageName, targetUser)); UserRecord userRecord = getOrCreateUserRecordLocked(targetUser.getIdentifier()); managerRecord = new ManagerRecord( userRecord, manager, callerUid, callerPid, callerPackageName, targetPackageName); try { binder.linkToDeath(managerRecord, 0); } catch (RemoteException ex) { Loading Loading @@ -1791,22 +1858,30 @@ class MediaRouter2ServiceImpl { } final class ManagerRecord implements IBinder.DeathRecipient { public final UserRecord mUserRecord; public final IMediaRouter2Manager mManager; @NonNull public final UserRecord mUserRecord; @NonNull public final IMediaRouter2Manager mManager; public final int mOwnerUid; public final int mOwnerPid; public final String mOwnerPackageName; @NonNull public final String mOwnerPackageName; public final int mManagerId; public SessionCreationRequest mLastSessionCreationRequest; // TODO (b/281072508): Document behaviour around nullability for mTargetPackageName. @Nullable public final String mTargetPackageName; @Nullable public SessionCreationRequest mLastSessionCreationRequest; public boolean mIsScanning; ManagerRecord(UserRecord userRecord, IMediaRouter2Manager manager, int ownerUid, int ownerPid, String ownerPackageName) { ManagerRecord( @NonNull UserRecord userRecord, @NonNull IMediaRouter2Manager manager, int ownerUid, int ownerPid, @NonNull String ownerPackageName, @Nullable String targetPackageName) { mUserRecord = userRecord; mManager = manager; mOwnerUid = ownerUid; mOwnerPid = ownerPid; mOwnerPackageName = ownerPackageName; mTargetPackageName = targetPackageName; mManagerId = mNextRouterOrManagerId.getAndIncrement(); } Loading Loading
core/api/current.txt +1 −0 Original line number Diff line number Diff line Loading @@ -24168,6 +24168,7 @@ package android.media { method @Nullable public android.media.MediaRouter2.RoutingController getController(@NonNull String); method @NonNull public java.util.List<android.media.MediaRouter2.RoutingController> getControllers(); method @NonNull public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context); method @FlaggedApi("com.android.media.flags.enable_cross_user_routing_in_media_router2") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MEDIA_CONTENT_CONTROL, android.Manifest.permission.MEDIA_ROUTING_CONTROL}) public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.os.UserHandle); method @FlaggedApi("com.android.media.flags.enable_rlp_callbacks_in_media_router2") @Nullable public android.media.RouteListingPreference getRouteListingPreference(); method @NonNull public java.util.List<android.media.MediaRoute2Info> getRoutes(); method @NonNull public android.media.MediaRouter2.RoutingController getSystemController();
media/java/android/media/IMediaRouterService.aidl +2 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; import android.os.Bundle; import android.os.UserHandle; /** * {@hide} Loading @@ -50,7 +51,6 @@ interface IMediaRouterService { // MediaRouterService.java for readability. // Methods for MediaRouter2 boolean verifyPackageExists(String clientPackageName); List<MediaRoute2Info> getSystemRoutes(); RoutingSessionInfo getSystemSessionInfo(); Loading @@ -76,6 +76,7 @@ interface IMediaRouterService { List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager); RoutingSessionInfo getSystemSessionInfoForPackage(String packageName); void registerManager(IMediaRouter2Manager manager, String packageName); void registerProxyRouter(IMediaRouter2Manager manager, String callingPackageName, String targetPackageName, in UserHandle targetUser); void unregisterManager(IMediaRouter2Manager manager); void setRouteVolumeWithManager(IMediaRouter2Manager manager, int requestId, in MediaRoute2Info route, int volume); Loading
media/java/android/media/MediaRouter2.java +140 −52 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.media; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2; import static com.android.media.flags.Flags.FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2; import android.Manifest; import android.annotation.CallbackExecutor; Loading @@ -32,8 +33,10 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; Loading Loading @@ -78,8 +81,11 @@ 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; private record PackageNameUserHandlePair(String packageName, UserHandle user) {} @GuardedBy("sSystemRouterLock") private static final Map<String, MediaRouter2> sSystemMediaRouter2Map = new ArrayMap<>(); private static final Map<PackageNameUserHandlePair, MediaRouter2> sAppToProxyRouterMap = new ArrayMap<>(); @GuardedBy("sRouterLock") private static MediaRouter2 sInstance; Loading Loading @@ -161,66 +167,121 @@ public final class MediaRouter2 { } /** * Gets an instance of the system media router which controls the app's media routing. Returns * {@code null} if the given package name is invalid. There are several things to note when * using the media routers created with this method. * * <p>First of all, 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. * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app * specified by {@code clientPackageName}. Returns {@code null} if the specified package name * does not exist. * * <p>Also, do not keep/compare the instances of the {@link RoutingController}, since they are * always newly created with the latest session information whenever below methods are called: * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances: * * <ul> * <li>{@link #getControllers()} * <li>{@link #getController(String)} * <li>{@link TransferCallback#onTransfer(RoutingController, RoutingController)} * <li>{@link TransferCallback#onStop(RoutingController)} * <li>{@link ControllerCallback#onControllerUpdated(RoutingController)} * <li> * <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery * preference} passed by a proxy router. Use {@link RouteDiscoveryPreference#EMPTY} when * setting a route callback. * <li> * <p>Methods returning non-system {@link RoutingController controllers} always return * new instances with the latest data. Do not attempt to compare or store them. Instead, * use {@link #getController(String)} or {@link #getControllers()} to query the most * up-to-date state. * <li> * <p>Calls to {@link #setOnGetControllerHintsListener} are ignored. * </ul> * * Therefore, in order to track the current routing status, keep the controller's ID instead, * and use {@link #getController(String)} and {@link #getSystemController()} for getting * controllers. * * <p>Finally, it will have no effect to call {@link #setOnGetControllerHintsListener}. * * @param clientPackageName the package name of the app to control * @throws SecurityException if the caller doesn't have {@link * Manifest.permission#MEDIA_CONTENT_CONTROL MEDIA_CONTENT_CONTROL} permission. * @hide */ // TODO (b/311711420): Deprecate once #getInstance(Context, Looper, String, UserHandle) // reaches public SDK. @SystemApi @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) @Nullable public static MediaRouter2 getInstance( @NonNull Context context, @NonNull String clientPackageName) { Objects.requireNonNull(context, "context must not be null"); Objects.requireNonNull(clientPackageName, "clientPackageName must not be null"); // Note: Even though this check could be somehow bypassed, the other permission checks // in system server will not allow MediaRouter2Manager to be registered. IMediaRouterService serviceBinder = IMediaRouterService.Stub.asInterface( ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); // Capturing the IAE here to not break nullability. try { // verifyPackageExists throws SecurityException if the caller doesn't hold // MEDIA_CONTENT_CONTROL permission. if (!serviceBinder.verifyPackageExists(clientPackageName)) { return findOrCreateProxyInstanceForCallingUser( context, Looper.getMainLooper(), clientPackageName, context.getUser()); } catch (IllegalArgumentException ex) { Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring."); return null; } } catch (RemoteException e) { e.rethrowFromSystemServer(); } /** * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app * specified by {@code clientPackageName} and {@code user}. * * <p>You can specify any {@link Looper} of choice on which internal state updates will run. * * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances: * * <ul> * <li> * <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery * preference} passed by a proxy router. Use a {@link RouteDiscoveryPreference} with empty * {@link RouteDiscoveryPreference.Builder#setPreferredFeatures(List) preferred features} * when setting a route callback. * <li> * <p>Methods returning non-system {@link RoutingController controllers} always return * new instances with the latest data. Do not attempt to compare or store them. Instead, * use {@link #getController(String)} or {@link #getControllers()} to query the most * up-to-date state. * <li> * <p>Calls to {@link #setOnGetControllerHintsListener} are ignored. * </ul> * * @param context The {@link Context} of the caller. * @param looper The {@link Looper} on which to process internal state changes. * @param clientPackageName The package name of the app you want to control the routing of. * @param user The {@link UserHandle} of the user running the app for which to get the proxy * router instance. Must match {@link Process#myUserHandle()} if the caller doesn't hold * {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}. * @throws SecurityException if {@code user} does not match {@link Process#myUserHandle()} and * the caller does not hold {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}. * @throws IllegalArgumentException if {@code clientPackageName} does not exist in {@code user}. */ @FlaggedApi(FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2) @RequiresPermission( anyOf = { Manifest.permission.MEDIA_CONTENT_CONTROL, Manifest.permission.MEDIA_ROUTING_CONTROL }) @NonNull public static MediaRouter2 getInstance( @NonNull Context context, @NonNull Looper looper, @NonNull String clientPackageName, @NonNull UserHandle user) { return findOrCreateProxyInstanceForCallingUser(context, looper, clientPackageName, user); } /** * Returns the per-process singleton proxy router instance for the {@code clientPackageName} and * {@code user} if it exists, or otherwise it creates the appropriate instance. * * <p>If no instance has been created previously, the method will create an instance via {@link * #MediaRouter2(Context, Looper, String, UserHandle)}. */ @NonNull private static MediaRouter2 findOrCreateProxyInstanceForCallingUser( Context context, Looper looper, String clientPackageName, UserHandle user) { Objects.requireNonNull(context, "context must not be null"); Objects.requireNonNull(looper, "looper must not be null"); Objects.requireNonNull(user, "user must not be null"); if (TextUtils.isEmpty(clientPackageName)) { throw new IllegalArgumentException("clientPackageName must not be null or empty"); } PackageNameUserHandlePair key = new PackageNameUserHandlePair(clientPackageName, user); synchronized (sSystemRouterLock) { MediaRouter2 instance = sSystemMediaRouter2Map.get(clientPackageName); MediaRouter2 instance = sAppToProxyRouterMap.get(key); if (instance == null) { instance = new MediaRouter2(context, clientPackageName); sSystemMediaRouter2Map.put(clientPackageName, instance); instance = new MediaRouter2(context, looper, clientPackageName, user); sAppToProxyRouterMap.put(key, instance); } return instance; } Loading Loading @@ -304,9 +365,10 @@ public final class MediaRouter2 { mSystemController = new SystemRoutingController(currentSystemSessionInfo); } private MediaRouter2(Context context, String clientPackageName) { private MediaRouter2( Context context, Looper looper, String clientPackageName, UserHandle user) { mContext = context; mHandler = new Handler(Looper.getMainLooper()); mHandler = new Handler(looper); mMediaRouterService = IMediaRouterService.Stub.asInterface( ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); Loading @@ -315,7 +377,7 @@ public final class MediaRouter2 { new SystemRoutingController( ProxyMediaRouter2Impl.getSystemSessionInfoImpl( mMediaRouterService, clientPackageName)); mImpl = new ProxyMediaRouter2Impl(context, clientPackageName); mImpl = new ProxyMediaRouter2Impl(context, clientPackageName, user); } /** Loading Loading @@ -2000,25 +2062,30 @@ public final class MediaRouter2 { */ private class ProxyMediaRouter2Impl implements MediaRouter2Impl { // Fields originating from MediaRouter2Manager. private final MediaRouter2Manager mManager; private final IMediaRouter2Manager.Stub mClient; private final CopyOnWriteArrayList<MediaRouter2Manager.TransferRequest> mTransferRequests = new CopyOnWriteArrayList<>(); private final AtomicInteger mScanRequestCount = new AtomicInteger(/* initialValue= */ 0); // Fields originating from MediaRouter2. @NonNull private final String mClientPackageName; // TODO(b/281072508): Implement scan request counting when MediaRouter2Manager is removed. @NonNull private final UserHandle mClientUser; private final AtomicBoolean mIsScanning = new AtomicBoolean(/* initialValue= */ false); ProxyMediaRouter2Impl(@NonNull Context context, @NonNull String clientPackageName) { mManager = MediaRouter2Manager.getInstance(context.getApplicationContext()); ProxyMediaRouter2Impl( @NonNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user) { mClientUser = user; mClientPackageName = clientPackageName; mClient = new Client(); try { mMediaRouterService.registerManager( mClient, context.getApplicationContext().getPackageName()); mMediaRouterService.registerProxyRouter( mClient, context.getApplicationContext().getPackageName(), clientPackageName, user); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } Loading @@ -2029,14 +2096,35 @@ public final class MediaRouter2 { @Override public void startScan() { if (!mIsScanning.getAndSet(true)) { mManager.registerScanRequest(); if (mScanRequestCount.getAndIncrement() == 0) { try { mMediaRouterService.startScan(mClient); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } } } @Override public void stopScan() { if (mIsScanning.getAndSet(false)) { mManager.unregisterScanRequest(); if (mScanRequestCount.updateAndGet( count -> { if (count == 0) { throw new IllegalStateException( "No active scan requests to unregister."); } else { return --count; } }) == 0) { try { mMediaRouterService.stopScan(mClient); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } } } Loading
media/java/android/media/flags/media_better_together.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -55,3 +55,10 @@ flag { description: "Allow access to privileged routing capabilities to MEDIA_ROUTING_CONTROL holders." bug: "305919655" } flag { name: "enable_cross_user_routing_in_media_router2" namespace: "media_solutions" description: "Allows clients of privileged MediaRouter2 that hold INTERACT_ACROSS_USERS_FULL to control routing across users." bug: "288580225" }
services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +113 −38 Original line number Diff line number Diff line Loading @@ -193,26 +193,6 @@ class MediaRouter2ServiceImpl { // Start of methods that implement MediaRouter2 operations. @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) @NonNull public boolean verifyPackageExists(@NonNull String clientPackageName) { final int pid = Binder.getCallingPid(); final int uid = Binder.getCallingUid(); final long token = Binder.clearCallingIdentity(); try { // TODO (b/305919655) - Handle revoking of MEDIA_ROUTING_CONTROL at runtime. enforcePrivilegedRoutingPermissions(uid, pid, /* callerPackageName */ null); PackageManager pm = mContext.getPackageManager(); pm.getPackageInfo(clientPackageName, PackageManager.PackageInfoFlags.of(0)); return true; } catch (PackageManager.NameNotFoundException ex) { return false; } finally { Binder.restoreCallingIdentity(token); } } @NonNull public List<MediaRoute2Info> getSystemRoutes() { final int uid = Binder.getCallingUid(); Loading Loading @@ -491,13 +471,65 @@ class MediaRouter2ServiceImpl { final int callerUid = Binder.getCallingUid(); final int callerPid = Binder.getCallingPid(); final int callerUserId = UserHandle.getUserHandleForUid(callerUid).getIdentifier(); final UserHandle callerUser = Binder.getCallingUserHandle(); // TODO (b/305919655) - Handle revoking of MEDIA_ROUTING_CONTROL at runtime. enforcePrivilegedRoutingPermissions(callerUid, callerPid, callerPackageName); final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { registerManagerLocked( manager, callerUid, callerPid, callerPackageName, /* targetPackageName */ null, callerUser); } } finally { Binder.restoreCallingIdentity(token); } } @RequiresPermission( anyOf = { Manifest.permission.MEDIA_CONTENT_CONTROL, Manifest.permission.MEDIA_ROUTING_CONTROL }) public void registerProxyRouter( @NonNull IMediaRouter2Manager manager, @NonNull String callerPackageName, @NonNull String targetPackageName, @NonNull UserHandle targetUser) { Objects.requireNonNull(manager, "manager must not be null"); Objects.requireNonNull(targetUser, "targetUser must not be null"); if (TextUtils.isEmpty(targetPackageName)) { throw new IllegalArgumentException("targetPackageName must not be empty"); } int callerUid = Binder.getCallingUid(); int callerPid = Binder.getCallingPid(); final long token = Binder.clearCallingIdentity(); try { // TODO (b/305919655) - Handle revoking of MEDIA_ROUTING_CONTROL at runtime. enforcePrivilegedRoutingPermissions(callerUid, callerPid, callerPackageName); enforceCrossUserPermissions(callerUid, callerPid, targetUser); if (!verifyPackageExistsForUser(targetPackageName, targetUser)) { throw new IllegalArgumentException( "targetPackageName does not exist: " + targetPackageName); } synchronized (mLock) { registerManagerLocked( manager, callerUid, callerPid, callerPackageName, callerUserId); manager, callerUid, callerPid, callerPackageName, targetPackageName, targetUser); } } finally { Binder.restoreCallingIdentity(token); Loading Loading @@ -761,6 +793,37 @@ class MediaRouter2ServiceImpl { } } @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS) private boolean verifyPackageExistsForUser( @NonNull String clientPackageName, @NonNull UserHandle user) { try { PackageManager pm = mContext.getPackageManager(); pm.getPackageInfoAsUser( clientPackageName, PackageManager.PackageInfoFlags.of(0), user.getIdentifier()); return true; } catch (PackageManager.NameNotFoundException ex) { return false; } } /** * Enforces the caller has {@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} if the * caller's user is different from the target user. */ private void enforceCrossUserPermissions( int callerUid, int callerPid, @NonNull UserHandle targetUser) { int callerUserId = UserHandle.getUserId(callerUid); if (targetUser.getIdentifier() != callerUserId) { mContext.enforcePermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, callerPid, callerUid, "Must hold INTERACT_ACROSS_USERS_FULL to control an app in a different" + " userId."); } } // End of methods that implements operations for both MediaRouter2 and MediaRouter2Manager. public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { Loading Loading @@ -1203,7 +1266,8 @@ class MediaRouter2ServiceImpl { int callerUid, int callerPid, @NonNull String callerPackageName, int callerUserId) { @Nullable String targetPackageName, @NonNull UserHandle targetUser) { final IBinder binder = manager.asBinder(); ManagerRecord managerRecord = mAllManagerRecords.get(binder); Loading @@ -1217,15 +1281,18 @@ class MediaRouter2ServiceImpl { TAG, TextUtils.formatSimple( "registerManager | callerUid: %d, callerPid: %d, callerPackage: %s," + " callerUserId: %d", callerUid, callerPid, callerPackageName, callerUserId)); // TODO (b/305919655) - Handle revoking of MEDIA_ROUTING_CONTROL at runtime. enforcePrivilegedRoutingPermissions(callerUid, callerPid, callerPackageName); UserRecord userRecord = getOrCreateUserRecordLocked(callerUserId); managerRecord = new ManagerRecord( userRecord, manager, callerUid, callerPid, callerPackageName); + "targetPackageName: %s, targetUserId: %d", callerUid, callerPid, callerPackageName, targetPackageName, targetUser)); UserRecord userRecord = getOrCreateUserRecordLocked(targetUser.getIdentifier()); managerRecord = new ManagerRecord( userRecord, manager, callerUid, callerPid, callerPackageName, targetPackageName); try { binder.linkToDeath(managerRecord, 0); } catch (RemoteException ex) { Loading Loading @@ -1791,22 +1858,30 @@ class MediaRouter2ServiceImpl { } final class ManagerRecord implements IBinder.DeathRecipient { public final UserRecord mUserRecord; public final IMediaRouter2Manager mManager; @NonNull public final UserRecord mUserRecord; @NonNull public final IMediaRouter2Manager mManager; public final int mOwnerUid; public final int mOwnerPid; public final String mOwnerPackageName; @NonNull public final String mOwnerPackageName; public final int mManagerId; public SessionCreationRequest mLastSessionCreationRequest; // TODO (b/281072508): Document behaviour around nullability for mTargetPackageName. @Nullable public final String mTargetPackageName; @Nullable public SessionCreationRequest mLastSessionCreationRequest; public boolean mIsScanning; ManagerRecord(UserRecord userRecord, IMediaRouter2Manager manager, int ownerUid, int ownerPid, String ownerPackageName) { ManagerRecord( @NonNull UserRecord userRecord, @NonNull IMediaRouter2Manager manager, int ownerUid, int ownerPid, @NonNull String ownerPackageName, @Nullable String targetPackageName) { mUserRecord = userRecord; mManager = manager; mOwnerUid = ownerUid; mOwnerPid = ownerPid; mOwnerPackageName = ownerPackageName; mTargetPackageName = targetPackageName; mManagerId = mNextRouterOrManagerId.getAndIncrement(); } Loading