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

Commit 02406603 authored by Santiago Seifert's avatar Santiago Seifert
Browse files

Add DISABLE_REASON_APP_ONLY and parameter to navigate to app

With this functionality, apps can disable specific routes
in the output switcher so that they are only available in
the app. A usecase for this is customizing the way the
routing session is joined.

This change also adds a ComponentName which the system can
use to navigate to the app when the user taps on a route
disabled using DISABLE_REASON_APP_ONLY.

Test: atest MediaRouter2HostSideTest
Bug: 241888071
Bug: 235352899
Change-Id: Ia291dfb9ad9a940b10f2c1b759dc1ee205df6cc2
parent 5d7d1378
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -24257,15 +24257,19 @@ package android.media {
  public final class RouteListingPreference implements android.os.Parcelable {
    method public int describeContents();
    method @Nullable public android.content.ComponentName getInAppOnlyItemRoutingReceiver();
    method @NonNull public java.util.List<android.media.RouteListingPreference.Item> getItems();
    method public boolean getUseSystemOrdering();
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field public static final String ACTION_TRANSFER_MEDIA = "android.media.action.TRANSFER_MEDIA";
    field @NonNull public static final android.os.Parcelable.Creator<android.media.RouteListingPreference> CREATOR;
    field public static final String EXTRA_ROUTE_ID = "android.media.extra.ROUTE_ID";
  }
  public static final class RouteListingPreference.Builder {
    ctor public RouteListingPreference.Builder();
    method @NonNull public android.media.RouteListingPreference build();
    method @NonNull public android.media.RouteListingPreference.Builder setInAppOnlyItemRoutingReceiver(@Nullable android.content.ComponentName);
    method @NonNull public android.media.RouteListingPreference.Builder setItems(@NonNull java.util.List<android.media.RouteListingPreference.Item>);
    method @NonNull public android.media.RouteListingPreference.Builder setUseSystemOrdering(boolean);
  }
@@ -24280,6 +24284,7 @@ package android.media {
    field @NonNull public static final android.os.Parcelable.Creator<android.media.RouteListingPreference.Item> CREATOR;
    field public static final int DISABLE_REASON_AD = 3; // 0x3
    field public static final int DISABLE_REASON_DOWNLOADED_CONTENT = 2; // 0x2
    field public static final int DISABLE_REASON_IN_APP_ONLY = 4; // 0x4
    field public static final int DISABLE_REASON_NONE = 0; // 0x0
    field public static final int DISABLE_REASON_SUBSCRIPTION_REQUIRED = 1; // 0x1
    field public static final int FLAG_ONGOING_SESSION = 1; // 0x1
+68 −4
Original line number Diff line number Diff line
@@ -19,6 +19,9 @@ package android.media;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
@@ -40,6 +43,18 @@ import java.util.Objects;
 */
public final class RouteListingPreference implements Parcelable {

    /**
     * {@link Intent} action for apps to take the user to a screen for transferring media playback
     * to the route with the id provided by the extra with key {@link #EXTRA_ROUTE_ID}.
     */
    public static final String ACTION_TRANSFER_MEDIA = "android.media.action.TRANSFER_MEDIA";

    /**
     * {@link Intent} string extra key that contains the {@link Item#getRouteId() id} of the route
     * to transfer to, as part of an {@link #ACTION_TRANSFER_MEDIA} intent.
     */
    public static final String EXTRA_ROUTE_ID = "android.media.extra.ROUTE_ID";

    @NonNull
    public static final Creator<RouteListingPreference> CREATOR =
            new Creator<>() {
@@ -56,10 +71,12 @@ public final class RouteListingPreference implements Parcelable {

    @NonNull private final List<Item> mItems;
    private final boolean mUseSystemOrdering;
    @Nullable private final ComponentName mInAppOnlyItemRoutingReceiver;

    private RouteListingPreference(Builder builder) {
        mItems = builder.mItems;
        mUseSystemOrdering = builder.mUseSystemOrdering;
        mInAppOnlyItemRoutingReceiver = builder.mInAppOnlyItemRoutingReceiver;
    }

    private RouteListingPreference(Parcel in) {
@@ -67,6 +84,7 @@ public final class RouteListingPreference implements Parcelable {
                in.readParcelableList(new ArrayList<>(), Item.class.getClassLoader(), Item.class);
        mItems = List.copyOf(items);
        mUseSystemOrdering = in.readBoolean();
        mInAppOnlyItemRoutingReceiver = ComponentName.readFromParcel(in);
    }

    /**
@@ -90,6 +108,21 @@ public final class RouteListingPreference implements Parcelable {
        return mUseSystemOrdering;
    }

    /**
     * Returns a {@link ComponentName} for handling routes disabled via {@link
     * Item#DISABLE_REASON_IN_APP_ONLY}, or null if the user needs to manually navigate to the app
     * in order to route to select the corresponding routes.
     *
     * <p>If the user selects an {@link Item} disabled via {@link Item#DISABLE_REASON_IN_APP_ONLY},
     * and this method returns a non-null {@link ComponentName}, the system takes the user back to
     * the app by launching an intent to the returned {@link ComponentName}, using action {@link
     * #ACTION_TRANSFER_MEDIA}, with the extra {@link #EXTRA_ROUTE_ID}.
     */
    @Nullable
    public ComponentName getInAppOnlyItemRoutingReceiver() {
        return mInAppOnlyItemRoutingReceiver;
    }

    // RouteListingPreference Parcelable implementation.

    @Override
@@ -101,6 +134,7 @@ public final class RouteListingPreference implements Parcelable {
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeParcelableList(mItems, flags);
        dest.writeBoolean(mUseSystemOrdering);
        ComponentName.writeToParcel(mInAppOnlyItemRoutingReceiver, dest);
    }

    // Equals and hashCode.
@@ -114,12 +148,15 @@ public final class RouteListingPreference implements Parcelable {
            return false;
        }
        RouteListingPreference that = (RouteListingPreference) other;
        return mItems.equals(that.mItems) && mUseSystemOrdering == that.mUseSystemOrdering;
        return mItems.equals(that.mItems)
                && mUseSystemOrdering == that.mUseSystemOrdering
                && Objects.equals(
                        mInAppOnlyItemRoutingReceiver, that.mInAppOnlyItemRoutingReceiver);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mItems, mUseSystemOrdering);
        return Objects.hash(mItems, mUseSystemOrdering, mInAppOnlyItemRoutingReceiver);
    }

    /** Builder for {@link RouteListingPreference}. */
@@ -127,6 +164,7 @@ public final class RouteListingPreference implements Parcelable {

        private List<Item> mItems;
        private boolean mUseSystemOrdering;
        private ComponentName mInAppOnlyItemRoutingReceiver;

        /** Creates a new instance with default values (documented in the setters). */
        public Builder() {
@@ -158,6 +196,18 @@ public final class RouteListingPreference implements Parcelable {
            return this;
        }

        /**
         * See {@link #getInAppOnlyItemRoutingReceiver()}.
         *
         * <p>The default value is {@code null}.
         */
        @NonNull
        public Builder setInAppOnlyItemRoutingReceiver(
                @Nullable ComponentName inAppOnlyItemRoutingReceiver) {
            mInAppOnlyItemRoutingReceiver = inAppOnlyItemRoutingReceiver;
            return this;
        }

        /**
         * Creates and returns a new {@link RouteListingPreference} instance with the given
         * parameters.
@@ -203,7 +253,8 @@ public final class RouteListingPreference implements Parcelable {
                    DISABLE_REASON_NONE,
                    DISABLE_REASON_SUBSCRIPTION_REQUIRED,
                    DISABLE_REASON_DOWNLOADED_CONTENT,
                    DISABLE_REASON_AD
                    DISABLE_REASON_AD,
                    DISABLE_REASON_IN_APP_ONLY
                })
        public @interface DisableReason {}

@@ -221,6 +272,14 @@ public final class RouteListingPreference implements Parcelable {
        public static final int DISABLE_REASON_DOWNLOADED_CONTENT = 2;
        /** The corresponding route is not available because an ad is in progress. */
        public static final int DISABLE_REASON_AD = 3;
        /**
         * The corresponding route is only available for routing from within the app.
         *
         * <p>The user may still select the corresponding route if the app provides an {@link
         * #getInAppOnlyItemRoutingReceiver() in-app routing receiver}, in which case the system
         * will take the user to the app.
         */
        public static final int DISABLE_REASON_IN_APP_ONLY = 4;

        @NonNull
        public static final Creator<Item> CREATOR =
@@ -257,7 +316,11 @@ public final class RouteListingPreference implements Parcelable {
            Preconditions.checkArgument(mSessionParticipantCount >= 0);
        }

        /** Returns the id of the route that corresponds to this route listing preference item. */
        /**
         * Returns the id of the route that corresponds to this route listing preference item.
         *
         * @see MediaRoute2Info#getId()
         */
        @NonNull
        public String getRouteId() {
            return mRouteId;
@@ -282,6 +345,7 @@ public final class RouteListingPreference implements Parcelable {
         * @see #DISABLE_REASON_SUBSCRIPTION_REQUIRED
         * @see #DISABLE_REASON_DOWNLOADED_CONTENT
         * @see #DISABLE_REASON_AD
         * @see #DISABLE_REASON_IN_APP_ONLY
         */
        @DisableReason
        public int getDisableReason() {
+16 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -277,6 +278,15 @@ class MediaRouter2ServiceImpl {
    public void setRouteListingPreference(
            @NonNull IMediaRouter2 router,
            @Nullable RouteListingPreference routeListingPreference) {
        ComponentName inAppOnlyItemRoutingReceiver =
                routeListingPreference != null
                        ? routeListingPreference.getInAppOnlyItemRoutingReceiver()
                        : null;
        if (inAppOnlyItemRoutingReceiver != null) {
            MediaServerUtils.enforcePackageName(
                    inAppOnlyItemRoutingReceiver.getPackageName(), Binder.getCallingUid());
        }

        final long token = Binder.clearCallingIdentity();
        try {
            synchronized (mLock) {
@@ -779,6 +789,12 @@ class MediaRouter2ServiceImpl {
                obtainMessage(UserHandler::notifyDiscoveryPreferenceChangedToManagers,
                        routerRecord.mUserRecord.mHandler,
                        routerRecord.mPackageName, null));
        routerRecord.mUserRecord.mHandler.sendMessage(
                obtainMessage(
                        UserHandler::notifyRouteListingPreferenceChangeToManagers,
                        routerRecord.mUserRecord.mHandler,
                        routerRecord.mPackageName,
                        /* routeListingPreference= */ null));
        userRecord.mHandler.sendMessage(
                obtainMessage(UserHandler::updateDiscoveryPreferenceOnHandler,
                        userRecord.mHandler));
+39 −0
Original line number Diff line number Diff line
@@ -18,7 +18,13 @@ package com.android.server.media;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.os.Binder;
import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;

import com.android.server.LocalServices;

import java.io.PrintWriter;

@@ -26,6 +32,39 @@ import java.io.PrintWriter;
 * Util class for media server.
 */
class MediaServerUtils {

    /**
     * Throws if the given {@code packageName} does not correspond to the given {@code uid}.
     *
     * <p>This method trusts calls from {@link Process#ROOT_UID} and {@link Process#SHELL_UID}.
     *
     * @param packageName A package name to verify (usually sent over binder by an app).
     * @param uid The calling uid, obtained via {@link Binder#getCallingUid()}.
     * @throws IllegalArgumentException If the given {@code packageName} does not correspond to the
     *     given {@code uid}, and {@code uid} is not the root uid, or the shell uid.
     */
    public static void enforcePackageName(String packageName, int uid) {
        if (uid == Process.ROOT_UID || uid == Process.SHELL_UID) {
            return;
        }
        if (TextUtils.isEmpty(packageName)) {
            throw new IllegalArgumentException("packageName may not be empty");
        }
        final PackageManagerInternal packageManagerInternal =
                LocalServices.getService(PackageManagerInternal.class);
        final int actualUid =
                packageManagerInternal.getPackageUid(
                        packageName, 0 /* flags */, UserHandle.getUserId(uid));
        if (!UserHandle.isSameApp(uid, actualUid)) {
            throw new IllegalArgumentException(
                    "packageName does not belong to the calling uid; "
                            + "pkg="
                            + packageName
                            + ", uid="
                            + uid);
        }
    }

    /**
     * Verify that caller holds {@link android.Manifest.permission#DUMP}.
     */
+6 −25
Original line number Diff line number Diff line
@@ -538,30 +538,11 @@ public class MediaSessionService extends SystemService implements Monitor {
        mHandler.postSessionsChanged(session);
    }

    private void enforcePackageName(String packageName, int uid) {
        if (TextUtils.isEmpty(packageName)) {
            throw new IllegalArgumentException("packageName may not be empty");
        }
        if (uid == Process.ROOT_UID || uid == Process.SHELL_UID) {
            // If the caller is shell, then trust the packageName given and allow it
            // to proceed.
            return;
        }
        final PackageManagerInternal packageManagerInternal =
                LocalServices.getService(PackageManagerInternal.class);
        final int actualUid = packageManagerInternal.getPackageUid(
                packageName, 0 /* flags */, UserHandle.getUserId(uid));
        if (!UserHandle.isSameApp(uid, actualUid)) {
            throw new IllegalArgumentException("packageName does not belong to the calling uid; "
                    + "pkg=" + packageName + ", uid=" + uid);
        }
    }

    void tempAllowlistTargetPkgIfPossible(int targetUid, String targetPackage,
            int callingPid, int callingUid, String callingPackage, String reason) {
        final long token = Binder.clearCallingIdentity();
        try {
            enforcePackageName(callingPackage, callingUid);
            MediaServerUtils.enforcePackageName(callingPackage, callingUid);
            if (targetUid != callingUid) {
                boolean canAllowWhileInUse = mActivityManagerLocal
                        .canAllowWhileInUsePermissionInFgs(callingPid, callingUid, callingPackage);
@@ -1206,7 +1187,7 @@ public class MediaSessionService extends SystemService implements Monitor {
            final int uid = Binder.getCallingUid();
            final long token = Binder.clearCallingIdentity();
            try {
                enforcePackageName(packageName, uid);
                MediaServerUtils.enforcePackageName(packageName, uid);
                int resolvedUserId = handleIncomingUser(pid, uid, userId, packageName);
                if (cb == null) {
                    throw new IllegalArgumentException("Controller callback cannot be null");
@@ -1258,7 +1239,7 @@ public class MediaSessionService extends SystemService implements Monitor {
            final int userId = userHandle.getIdentifier();
            final long token = Binder.clearCallingIdentity();
            try {
                enforcePackageName(packageName, uid);
                MediaServerUtils.enforcePackageName(packageName, uid);
                enforceMediaPermissions(packageName, pid, uid, userId);

                MediaSessionRecordImpl record;
@@ -1289,7 +1270,7 @@ public class MediaSessionService extends SystemService implements Monitor {
            final int userId = userHandle.getIdentifier();
            final long token = Binder.clearCallingIdentity();
            try {
                enforcePackageName(packageName, uid);
                MediaServerUtils.enforcePackageName(packageName, uid);
                enforceMediaPermissions(packageName, pid, uid, userId);

                MediaSessionRecordImpl record;
@@ -1615,7 +1596,7 @@ public class MediaSessionService extends SystemService implements Monitor {
            final int userId = userHandle.getIdentifier();
            final long token = Binder.clearCallingIdentity();
            try {
                enforcePackageName(packageName, uid);
                MediaServerUtils.enforcePackageName(packageName, uid);
                enforceMediaPermissions(packageName, pid, uid, userId);

                synchronized (mLock) {
@@ -2129,7 +2110,7 @@ public class MediaSessionService extends SystemService implements Monitor {
                // If they gave us a component name verify they own the
                // package
                packageName = componentName.getPackageName();
                enforcePackageName(packageName, uid);
                MediaServerUtils.enforcePackageName(packageName, uid);
            }
            // Check that they can make calls on behalf of the user and get the final user id
            int resolvedUserId = handleIncomingUser(pid, uid, userId, packageName);