Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e2788a3e authored by Iván Budnik's avatar Iván Budnik Committed by Android (Google) Code Review
Browse files

Merge "Add support for privileged routing across users" into main

parents 996826c8 324f33f5
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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();
+2 −1
Original line number Diff line number Diff line
@@ -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}
@@ -50,7 +51,6 @@ interface IMediaRouterService {
    // MediaRouterService.java for readability.

    // Methods for MediaRouter2
    boolean verifyPackageExists(String clientPackageName);
    List<MediaRoute2Info> getSystemRoutes();
    RoutingSessionInfo getSystemSessionInfo();

@@ -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);
+140 −52
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
@@ -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;
        }
@@ -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));
@@ -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);
    }

    /**
@@ -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();
            }
@@ -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();
                    }
                }
            }
        }

+7 −0
Original line number Diff line number Diff line
@@ -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"
}
+113 −38
Original line number Diff line number Diff line
@@ -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();
@@ -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);
@@ -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) {
@@ -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);

@@ -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) {
@@ -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