Add support for app route listing preferences

Test: atest MediaRouter2HostSideTest
Bug: 241888071
Bug: 235352899
Change-Id: Ic9545a035631f2adb79d46e6432356dd7af4f5a0
This commit is contained in:
Santiago Seifert
2022-11-11 17:45:12 +00:00
parent 16eaa6fe23
commit 71e5d2f4b6
9 changed files with 425 additions and 1 deletions

View File

@@ -23507,6 +23507,7 @@ package android.media {
method public void registerRouteCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaRouter2.RouteCallback, @NonNull android.media.RouteDiscoveryPreference);
method public void registerTransferCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaRouter2.TransferCallback);
method public void setOnGetControllerHintsListener(@Nullable android.media.MediaRouter2.OnGetControllerHintsListener);
method public void setRouteListingPreference(@Nullable android.media.RouteListingPreference);
method public void stop();
method public void transferTo(@NonNull android.media.MediaRoute2Info);
method public void unregisterControllerCallback(@NonNull android.media.MediaRouter2.ControllerCallback);
@@ -23880,6 +23881,22 @@ package android.media {
method @NonNull public android.media.RouteDiscoveryPreference.Builder setShouldPerformActiveScan(boolean);
}
public final class RouteListingPreference implements android.os.Parcelable {
ctor public RouteListingPreference(@NonNull java.util.List<android.media.RouteListingPreference.Item>);
method public int describeContents();
method @NonNull public java.util.List<android.media.RouteListingPreference.Item> getItems();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.RouteListingPreference> CREATOR;
}
public static final class RouteListingPreference.Item implements android.os.Parcelable {
ctor public RouteListingPreference.Item(@NonNull String);
method public int describeContents();
method @NonNull public String getRouteId();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.RouteListingPreference.Item> CREATOR;
}
public final class RoutingSessionInfo implements android.os.Parcelable {
method public int describeContents();
method @NonNull public String getClientPackageName();

View File

@@ -19,6 +19,7 @@ package android.media;
import android.media.MediaRoute2ProviderInfo;
import android.media.MediaRoute2Info;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
/**
@@ -30,6 +31,8 @@ oneway interface IMediaRouter2Manager {
void notifySessionReleased(in RoutingSessionInfo session);
void notifyDiscoveryPreferenceChanged(String packageName,
in RouteDiscoveryPreference discoveryPreference);
void notifyRouteListingPreferenceChange(String packageName,
in @nullable RouteListingPreference routeListingPreference);
void notifyRoutesUpdated(in List<MediaRoute2Info> routes);
void notifyRequestFailed(int requestId, int reason);
}

View File

@@ -23,6 +23,7 @@ import android.media.IMediaRouterClient;
import android.media.MediaRoute2Info;
import android.media.MediaRouterClientState;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
import android.os.Bundle;
@@ -57,6 +58,8 @@ interface IMediaRouterService {
void unregisterRouter2(IMediaRouter2 router);
void setDiscoveryRequestWithRouter2(IMediaRouter2 router,
in RouteDiscoveryPreference preference);
void setRouteListingPreference(IMediaRouter2 router,
in @nullable RouteListingPreference routeListingPreference);
void setRouteVolumeWithRouter2(IMediaRouter2 router, in MediaRoute2Info route, int volume);
void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId, long managerRequestId,

View File

@@ -112,6 +112,10 @@ public final class MediaRouter2 {
@GuardedBy("mLock")
final Map<String, MediaRoute2Info> mRoutes = new ArrayMap<>();
@GuardedBy("mLock")
@Nullable
private RouteListingPreference mRouteListingPreference;
final RoutingController mSystemController;
@GuardedBy("mLock")
@@ -461,6 +465,52 @@ public final class MediaRouter2 {
}
}
/**
* Sets the {@link RouteListingPreference} of the app associated to this media router.
*
* <p>Use this method to inform the system UI of the routes that you would like to list for
* media routing, via the Output Switcher.
*
* <p>You should call this method before {@link #registerRouteCallback registering any route
* callbacks} and immediately after receiving any {@link RouteCallback#onRoutesUpdated route
* updates} in order to keep the system UI in a consistent state. You can also call this method
* at any other point to update the listing preference dynamically.
*
* <p>Notes:
*
* <ol>
* <li>You should not include the ids of two or more routes with a match in their {@link
* MediaRoute2Info#getDeduplicationIds() deduplication ids}. If you do, the system will
* deduplicate them using its own criteria.
* <li>You can use this method to rank routes in the output switcher, placing the more
* important routes first. The system might override the proposed ranking.
* <li>You can use this method to avoid listing routes using dynamic criteria. For example,
* you can limit access to a specific type of device according to runtime criteria.
* </ol>
*
* @param routeListingPreference The {@link RouteListingPreference} for the system to use for
* route listing. When null, the system uses its default listing criteria.
*/
public void setRouteListingPreference(@Nullable RouteListingPreference routeListingPreference) {
synchronized (mLock) {
if (Objects.equals(mRouteListingPreference, routeListingPreference)) {
// Nothing changed. We return early to save a call to the system server.
return;
}
mRouteListingPreference = routeListingPreference;
try {
if (mStub == null) {
MediaRouter2Stub stub = new MediaRouter2Stub();
mMediaRouterService.registerRouter2(stub, mPackageName);
mStub = stub;
}
mMediaRouterService.setRouteListingPreference(mStub, mRouteListingPreference);
} catch (RemoteException ex) {
ex.rethrowFromSystemServer();
}
}
}
@GuardedBy("mLock")
private boolean updateDiscoveryPreferenceIfNeededLocked() {
RouteDiscoveryPreference newDiscoveryPreference = new RouteDiscoveryPreference.Builder(

View File

@@ -35,6 +35,7 @@ import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.Collections;
@@ -93,6 +94,11 @@ public final class MediaRouter2Manager {
@NonNull
final ConcurrentMap<String, RouteDiscoveryPreference> mDiscoveryPreferenceMap =
new ConcurrentHashMap<>();
// TODO(b/241888071): Merge mDiscoveryPreferenceMap and mPackageToRouteListingPreferenceMap into
// a single record object maintained by a single package-to-record map.
@NonNull
private final ConcurrentMap<String, RouteListingPreference>
mPackageToRouteListingPreferenceMap = new ConcurrentHashMap<>();
private final AtomicInteger mNextRequestId = new AtomicInteger(1);
private final CopyOnWriteArrayList<TransferRequest> mTransferRequests =
@@ -354,6 +360,16 @@ public final class MediaRouter2Manager {
return mDiscoveryPreferenceMap.getOrDefault(packageName, RouteDiscoveryPreference.EMPTY);
}
/**
* Returns the {@link RouteListingPreference} of the app with the given {@code packageName}, or
* null if the app has not set any.
*/
@Nullable
public RouteListingPreference getRouteListingPreference(@NonNull String packageName) {
Preconditions.checkArgument(!TextUtils.isEmpty(packageName));
return mPackageToRouteListingPreferenceMap.get(packageName);
}
/**
* Gets the system routing session for the given {@code packageName}.
* Apps can select a route that is not the global route. (e.g. an app can select the device
@@ -686,6 +702,24 @@ public final class MediaRouter2Manager {
}
}
private void updateRouteListingPreference(
@NonNull String packageName, @Nullable RouteListingPreference routeListingPreference) {
RouteListingPreference oldRouteListingPreference =
routeListingPreference == null
? mPackageToRouteListingPreferenceMap.remove(packageName)
: mPackageToRouteListingPreferenceMap.put(
packageName, routeListingPreference);
if (Objects.equals(oldRouteListingPreference, routeListingPreference)) {
return;
}
for (CallbackRecord record : mCallbackRecords) {
record.mExecutor.execute(
() ->
record.mCallback.onRouteListingPreferenceUpdated(
packageName, routeListingPreference));
}
}
/**
* Gets the unmodifiable list of selected routes for the session.
*/
@@ -970,6 +1004,19 @@ public final class MediaRouter2Manager {
onPreferredFeaturesChanged(packageName, discoveryPreference.getPreferredFeatures());
}
/**
* Called when the app with the given {@code packageName} updates its {@link
* MediaRouter2#setRouteListingPreference route listing preference}.
*
* @param packageName The package name of the app that changed its listing preference.
* @param routeListingPreference The new {@link RouteListingPreference} set by the app with
* the given {@code packageName}. Maybe null if an app has unset its preference (by
* passing null to {@link MediaRouter2#setRouteListingPreference}).
*/
default void onRouteListingPreferenceUpdated(
@NonNull String packageName,
@Nullable RouteListingPreference routeListingPreference) {}
/**
* Called when a previous request has failed.
*
@@ -1055,6 +1102,17 @@ public final class MediaRouter2Manager {
MediaRouter2Manager.this, packageName, discoveryPreference));
}
@Override
public void notifyRouteListingPreferenceChange(
String packageName, @Nullable RouteListingPreference routeListingPreference) {
mHandler.sendMessage(
obtainMessage(
MediaRouter2Manager::updateRouteListingPreference,
MediaRouter2Manager.this,
packageName,
routeListingPreference));
}
@Override
public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
mHandler.sendMessage(

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.media;
parcelable RouteListingPreference;

View File

@@ -0,0 +1,183 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.media;
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Allows applications to customize the list of routes used for media routing (for example, in the
* System UI Output Switcher).
*
* @see MediaRouter2#setRouteListingPreference
*/
public final class RouteListingPreference implements Parcelable {
@NonNull
public static final Creator<RouteListingPreference> CREATOR =
new Creator<>() {
@Override
public RouteListingPreference createFromParcel(Parcel in) {
return new RouteListingPreference(in);
}
@Override
public RouteListingPreference[] newArray(int size) {
return new RouteListingPreference[size];
}
};
@NonNull private final List<Item> mItems;
/**
* Creates an instance with the given values.
*
* @param items See {@link #getItems()}.
*/
public RouteListingPreference(@NonNull List<Item> items) {
mItems = List.copyOf(Objects.requireNonNull(items));
}
private RouteListingPreference(Parcel in) {
List<Item> items =
in.readParcelableList(new ArrayList<>(), Item.class.getClassLoader(), Item.class);
mItems = List.copyOf(items);
}
/**
* Returns an unmodifiable list containing the items that the app wants to be listed for media
* routing.
*/
@NonNull
public List<Item> getItems() {
return mItems;
}
// RouteListingPreference Parcelable implementation.
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeParcelableList(mItems, flags);
}
// Equals and hashCode.
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof RouteListingPreference)) {
return false;
}
RouteListingPreference that = (RouteListingPreference) other;
return mItems.equals(that.mItems);
}
@Override
public int hashCode() {
return Objects.hash(mItems);
}
// Internal classes.
/** Holds preference information for a specific route in a media routing listing. */
public static final class Item implements Parcelable {
@NonNull
public static final Creator<Item> CREATOR =
new Creator<>() {
@Override
public Item createFromParcel(Parcel in) {
return new Item(in);
}
@Override
public Item[] newArray(int size) {
return new Item[size];
}
};
@NonNull private final String mRouteId;
/**
* Creates an instance with the given value.
*
* @param routeId See {@link #getRouteId()}. Must not be empty.
*/
public Item(@NonNull String routeId) {
Preconditions.checkArgument(!TextUtils.isEmpty(routeId));
mRouteId = routeId;
}
private Item(Parcel in) {
String routeId = in.readString();
Preconditions.checkArgument(!TextUtils.isEmpty(routeId));
mRouteId = routeId;
}
/** Returns the id of the route that corresponds to this route listing preference item. */
@NonNull
public String getRouteId() {
return mRouteId;
}
// Item Parcelable implementation.
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(mRouteId);
}
// Equals and hashCode.
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Item)) {
return false;
}
Item item = (Item) other;
return mRouteId.equals(item.mRouteId);
}
@Override
public int hashCode() {
return Objects.hash(mRouteId);
}
}
}

View File

@@ -41,6 +41,7 @@ import android.media.MediaRoute2ProviderInfo;
import android.media.MediaRoute2ProviderService;
import android.media.MediaRouter2Manager;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
import android.os.Binder;
import android.os.Bundle;
@@ -257,6 +258,24 @@ class MediaRouter2ServiceImpl {
}
}
public void setRouteListingPreference(
@NonNull IMediaRouter2 router,
@Nullable RouteListingPreference routeListingPreference) {
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
RouterRecord routerRecord = mAllRouterRecords.get(router.asBinder());
if (routerRecord == null) {
Slog.w(TAG, "Ignoring updating route listing of null routerRecord.");
return;
}
setRouteListingPreferenceLocked(routerRecord, routeListingPreference);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
public void setRouteVolumeWithRouter2(@NonNull IMediaRouter2 router,
@NonNull MediaRoute2Info route, int volume) {
Objects.requireNonNull(router, "router must not be null");
@@ -768,6 +787,31 @@ class MediaRouter2ServiceImpl {
routerRecord.mUserRecord.mHandler));
}
@GuardedBy("mLock")
private void setRouteListingPreferenceLocked(
RouterRecord routerRecord, @Nullable RouteListingPreference routeListingPreference) {
routerRecord.mRouteListingPreference = routeListingPreference;
String routeListingAsString =
routeListingPreference != null
? routeListingPreference.getItems().stream()
.map(RouteListingPreference.Item::getRouteId)
.collect(Collectors.joining(","))
: null;
mEventLogger.enqueue(
EventLogger.StringEvent.from(
"setRouteListingPreference",
"router id: %d, route listing preference: [%s]",
routerRecord.mRouterId,
routeListingAsString));
routerRecord.mUserRecord.mHandler.sendMessage(
obtainMessage(
UserHandler::notifyRouteListingPreferenceChangeToManagers,
routerRecord.mUserRecord.mHandler,
routerRecord.mPackageName,
routeListingPreference));
}
private void setRouteVolumeWithRouter2Locked(@NonNull IMediaRouter2 router,
@NonNull MediaRoute2Info route, int volume) {
final IBinder binder = router.asBinder();
@@ -1018,6 +1062,15 @@ class MediaRouter2ServiceImpl {
// RouteCallback#onRoutesAdded() for system MR2 will never be called with initial routes
// due to the lack of features.
for (RouterRecord routerRecord : userRecord.mRouterRecords) {
// Send route listing preferences before discovery preferences and routes to avoid an
// inconsistent state where there are routes to show, but the manager thinks
// the app has not expressed a preference for listing.
userRecord.mHandler.sendMessage(
obtainMessage(
UserHandler::notifyRouteListingPreferenceChangeToManagers,
routerRecord.mUserRecord.mHandler,
routerRecord.mPackageName,
routerRecord.mRouteListingPreference));
// TODO: UserRecord <-> routerRecord, why do they reference each other?
// How about removing mUserRecord from routerRecord?
routerRecord.mUserRecord.mHandler.sendMessage(
@@ -1397,6 +1450,7 @@ class MediaRouter2ServiceImpl {
public final int mRouterId;
public RouteDiscoveryPreference mDiscoveryPreference;
@Nullable public RouteListingPreference mRouteListingPreference;
RouterRecord(UserRecord userRecord, IMediaRouter2 router, int uid, int pid,
String packageName, boolean hasConfigureWifiDisplayPermission,
@@ -2424,6 +2478,34 @@ class MediaRouter2ServiceImpl {
}
}
private void notifyRouteListingPreferenceChangeToManagers(
String routerPackageName, @Nullable RouteListingPreference routeListingPreference) {
MediaRouter2ServiceImpl service = mServiceRef.get();
if (service == null) {
return;
}
List<IMediaRouter2Manager> managers = new ArrayList<>();
synchronized (service.mLock) {
for (ManagerRecord managerRecord : mUserRecord.mManagerRecords) {
managers.add(managerRecord.mManager);
}
}
for (IMediaRouter2Manager manager : managers) {
try {
manager.notifyRouteListingPreferenceChange(
routerPackageName, routeListingPreference);
} catch (RemoteException ex) {
Slog.w(
TAG,
"Failed to notify preferred features changed."
+ " Manager probably died.",
ex);
}
}
// TODO(b/238178508): In order to support privileged media router instances, we also
// need to update routers other than the one making the update.
}
private void notifyRequestFailedToManager(@NonNull IMediaRouter2Manager manager,
int requestId, int reason) {
try {
@@ -2505,7 +2587,6 @@ class MediaRouter2ServiceImpl {
}
return null;
}
}
static final class SessionCreationRequest {
public final RouterRecord mRouterRecord;

View File

@@ -17,6 +17,7 @@
package com.android.server.media;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.ActivityManager;
import android.app.UserSwitchObserver;
@@ -43,6 +44,7 @@ import android.media.MediaRouterClientState;
import android.media.RemoteDisplayState;
import android.media.RemoteDisplayState.RemoteDisplayInfo;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
import android.os.Binder;
import android.os.Bundle;
@@ -418,6 +420,14 @@ public final class MediaRouterService extends IMediaRouterService.Stub
mService2.setDiscoveryRequestWithRouter2(router, request);
}
// Binder call
@Override
public void setRouteListingPreference(
@NonNull IMediaRouter2 router,
@Nullable RouteListingPreference routeListingPreference) {
mService2.setRouteListingPreference(router, routeListingPreference);
}
// Binder call
@Override
public void setRouteVolumeWithRouter2(IMediaRouter2 router,