diff --git a/core/api/current.txt b/core/api/current.txt index 4e281ec567bce..5a6c8ee9d2d86 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -24326,15 +24326,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 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 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); method @NonNull public android.media.RouteListingPreference.Builder setUseSystemOrdering(boolean); } @@ -24349,6 +24353,7 @@ package android.media { field @NonNull public static final android.os.Parcelable.Creator 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 diff --git a/media/java/android/media/RouteListingPreference.java b/media/java/android/media/RouteListingPreference.java index b1d74d4304aa8..b03653c07161e 100644 --- a/media/java/android/media/RouteListingPreference.java +++ b/media/java/android/media/RouteListingPreference.java @@ -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 CREATOR = new Creator<>() { @@ -56,10 +71,12 @@ public final class RouteListingPreference implements Parcelable { @NonNull private final List 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. + * + *

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 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()}. + * + *

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. + * + *

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 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() { diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index cc485baef0c9b..90d75691c0e8a 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -31,6 +31,7 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityThread; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -285,6 +286,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) { @@ -787,6 +797,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)); diff --git a/services/core/java/com/android/server/media/MediaServerUtils.java b/services/core/java/com/android/server/media/MediaServerUtils.java index 5fa2b1ceaae92..a4a99afd510de 100644 --- a/services/core/java/com/android/server/media/MediaServerUtils.java +++ b/services/core/java/com/android/server/media/MediaServerUtils.java @@ -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}. + * + *

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}. */ diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index e51ed1b520a9c..3a20cd962667b 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -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);