From 71e5d2f4b6a29adc07756984564879815ee76086 Mon Sep 17 00:00:00 2001 From: Santiago Seifert Date: Fri, 11 Nov 2022 17:45:12 +0000 Subject: [PATCH] Add support for app route listing preferences Test: atest MediaRouter2HostSideTest Bug: 241888071 Bug: 235352899 Change-Id: Ic9545a035631f2adb79d46e6432356dd7af4f5a0 --- core/api/current.txt | 17 ++ .../android/media/IMediaRouter2Manager.aidl | 3 + .../android/media/IMediaRouterService.aidl | 3 + media/java/android/media/MediaRouter2.java | 50 +++++ .../android/media/MediaRouter2Manager.java | 58 ++++++ .../android/media/RouteListingPreference.aidl | 19 ++ .../android/media/RouteListingPreference.java | 183 ++++++++++++++++++ .../server/media/MediaRouter2ServiceImpl.java | 83 +++++++- .../server/media/MediaRouterService.java | 10 + 9 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 media/java/android/media/RouteListingPreference.aidl create mode 100644 media/java/android/media/RouteListingPreference.java diff --git a/core/api/current.txt b/core/api/current.txt index e7194df0f97d5..dfb3b583a2807 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -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); + method public int describeContents(); + method @NonNull public java.util.List getItems(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator 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 CREATOR; + } + public final class RoutingSessionInfo implements android.os.Parcelable { method public int describeContents(); method @NonNull public String getClientPackageName(); diff --git a/media/java/android/media/IMediaRouter2Manager.aidl b/media/java/android/media/IMediaRouter2Manager.aidl index 9f3c3ff890321..bb31859dabdba 100644 --- a/media/java/android/media/IMediaRouter2Manager.aidl +++ b/media/java/android/media/IMediaRouter2Manager.aidl @@ -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 routes); void notifyRequestFailed(int requestId, int reason); } diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 742207b2d9e81..bddda4ab5002c 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -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, diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 161ea255dfb46..04e1bfac61245 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -112,6 +112,10 @@ public final class MediaRouter2 { @GuardedBy("mLock") final Map 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. + * + *

Use this method to inform the system UI of the routes that you would like to list for + * media routing, via the Output Switcher. + * + *

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

Notes: + * + *

    + *
  1. 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. + *
  2. 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. + *
  3. 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. + *
+ * + * @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( diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index e403e246f3180..7786f619840ac 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -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 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 + mPackageToRouteListingPreferenceMap = new ConcurrentHashMap<>(); private final AtomicInteger mNextRequestId = new AtomicInteger(1); private final CopyOnWriteArrayList 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 routes) { mHandler.sendMessage( diff --git a/media/java/android/media/RouteListingPreference.aidl b/media/java/android/media/RouteListingPreference.aidl new file mode 100644 index 0000000000000..844dc8f7920d0 --- /dev/null +++ b/media/java/android/media/RouteListingPreference.aidl @@ -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; diff --git a/media/java/android/media/RouteListingPreference.java b/media/java/android/media/RouteListingPreference.java new file mode 100644 index 0000000000000..26b190b9fd815 --- /dev/null +++ b/media/java/android/media/RouteListingPreference.java @@ -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 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 mItems; + + /** + * Creates an instance with the given values. + * + * @param items See {@link #getItems()}. + */ + public RouteListingPreference(@NonNull List items) { + mItems = List.copyOf(Objects.requireNonNull(items)); + } + + private RouteListingPreference(Parcel in) { + List 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 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 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); + } + } +} diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index f5ec880ed2356..ba43510f21b4c 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -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 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; diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index c0340b1eaa13e..add11351aff0d 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -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,