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
This commit is contained in:
Santiago Seifert
2022-12-22 13:27:15 +00:00
parent 5d7d1378df
commit 0240660308
5 changed files with 134 additions and 29 deletions

View File

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

View File

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

View File

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

View File

@@ -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}.
*/

View File

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