diff options
10 files changed, 794 insertions, 0 deletions
diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl index 85bc8efe2750..e9590d50d719 100644 --- a/media/java/android/media/IMediaRouter2.aidl +++ b/media/java/android/media/IMediaRouter2.aidl @@ -18,6 +18,7 @@ package android.media; import android.media.MediaRoute2Info; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Bundle; import android.os.UserHandle; @@ -37,4 +38,6 @@ oneway interface IMediaRouter2 { */ void requestCreateSessionByManager(long uniqueRequestId, in RoutingSessionInfo oldSession, in MediaRoute2Info route); + void notifyDeviceSuggestionsUpdated(String suggestingPackageName, + in List<SuggestedDeviceInfo> suggestions); } diff --git a/media/java/android/media/IMediaRouter2Manager.aidl b/media/java/android/media/IMediaRouter2Manager.aidl index 21908b2ca2e0..1c399d6958bb 100644 --- a/media/java/android/media/IMediaRouter2Manager.aidl +++ b/media/java/android/media/IMediaRouter2Manager.aidl @@ -21,6 +21,7 @@ import android.media.MediaRoute2Info; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; /** * {@hide} @@ -33,6 +34,8 @@ oneway interface IMediaRouter2Manager { in RouteDiscoveryPreference discoveryPreference); void notifyRouteListingPreferenceChange(String packageName, in @nullable RouteListingPreference routeListingPreference); + void notifyDeviceSuggestionsUpdated(String packageName, String suggestingPackageName, + in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); void notifyRoutesUpdated(in List<MediaRoute2Info> routes); void notifyRequestFailed(int requestId, int reason); void invalidateInstance(); diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 961962f6a010..60881f4bfc30 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -25,6 +25,7 @@ import android.media.MediaRouterClientState; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Bundle; import android.os.UserHandle; /** @@ -72,6 +73,10 @@ interface IMediaRouterService { in MediaRoute2Info route); void setSessionVolumeWithRouter2(IMediaRouter2 router, String sessionId, int volume); void releaseSessionWithRouter2(IMediaRouter2 router, String sessionId); + void setDeviceSuggestionsWithRouter2(IMediaRouter2 router, + in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + @nullable Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2( + IMediaRouter2 router); // Methods for MediaRouter2Manager List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager); @@ -98,4 +103,8 @@ interface IMediaRouterService { String sessionId, int volume); void releaseSessionWithManager(IMediaRouter2Manager manager, int requestId, String sessionId); boolean showMediaOutputSwitcherWithProxyRouter(IMediaRouter2Manager manager); + void setDeviceSuggestionsWithManager(IMediaRouter2Manager manager, + in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + @nullable Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager( + IMediaRouter2Manager manager); } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 3af36a404c30..db305effac3f 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -22,6 +22,7 @@ import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES; import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL; import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2; import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING; +import static com.android.media.flags.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API; import android.Manifest; import android.annotation.CallbackExecutor; @@ -159,6 +160,8 @@ public final class MediaRouter2 { new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<ControllerCallbackRecord> mControllerCallbackRecords = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList<DeviceSuggestionsCallbackRecord> + mDeviceSuggestionsCallbackRecords = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<ControllerCreationRequest> mControllerCreationRequests = new CopyOnWriteArrayList<>(); @@ -198,6 +201,10 @@ public final class MediaRouter2 { @Nullable private RouteListingPreference mRouteListingPreference; + @GuardedBy("mLock") + @Nullable + private Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceInfo = new HashMap<>(); + /** * Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback * dispatch. This is only used to determine what callback a route should be assigned to (added, @@ -760,6 +767,27 @@ public final class MediaRouter2 { } /** + * Registers the given callback to be invoked when the {@link SuggestedDeviceInfo} of the target + * router changes. + * + * <p>Calls using a previously registered callback will overwrite the previous executor. + * + * @hide + */ + public void registerDeviceSuggestionsCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) { + Objects.requireNonNull(executor, "executor must not be null"); + Objects.requireNonNull(deviceSuggestionsCallback, "callback must not be null"); + + DeviceSuggestionsCallbackRecord record = + new DeviceSuggestionsCallbackRecord(executor, deviceSuggestionsCallback); + + mDeviceSuggestionsCallbackRecords.remove(record); + mDeviceSuggestionsCallbackRecords.add(record); + } + + /** * Unregisters the given callback to not receive {@link RouteListingPreference} change events. * * @see #registerRouteListingPreferenceUpdatedCallback(Executor, Consumer) @@ -779,6 +807,21 @@ public final class MediaRouter2 { } /** + * Unregisters the given callback to not receive {@link SuggestedDeviceInfo} change events. + * + * @see #registerDeviceSuggestionsCallback(Executor, DeviceSuggestionsCallback) + * @hide + */ + public void unregisterDeviceSuggestionsCallback(@NonNull DeviceSuggestionsCallback callback) { + Objects.requireNonNull(callback, "callback must not be null"); + + if (!mDeviceSuggestionsCallbackRecords.remove( + new DeviceSuggestionsCallbackRecord(/* executor */ null, callback))) { + Log.w(TAG, "unregisterDeviceSuggestionsCallback: Ignoring an unknown" + " callback"); + } + } + + /** * Shows the system output switcher dialog. * * <p>Should only be called when the context of MediaRouter2 is in the foreground and visible on @@ -832,6 +875,36 @@ public final class MediaRouter2 { } /** + * Sets the suggested devices. + * + * <p>Use this method to inform the system UI that this device is suggested in the Output + * Switcher and media controls. + * + * <p>You should pass null to this method to clear a previously set suggestion without setting a + * new one. + * + * @param suggestedDeviceInfo The {@link SuggestedDeviceInfo} the router suggests should be + * provided to the user. + * @hide + */ + @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API) + public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + mImpl.setDeviceSuggestions(suggestedDeviceInfo); + } + + /** + * Gets the current suggested devices. + * + * @return the suggested devices, keyed by the package name providing each suggestion list. + * @hide + */ + @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API) + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() { + return mImpl.getDeviceSuggestions(); + } + + /** * Returns the current {@link RouteListingPreference} of the target router. * * <p>If this instance was created using {@code #getInstance(Context, String)}, then it returns @@ -1518,6 +1591,17 @@ public final class MediaRouter2 { } } + private void notifyDeviceSuggestionsUpdated( + @NonNull String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> deviceSuggestions) { + for (DeviceSuggestionsCallbackRecord record : mDeviceSuggestionsCallbackRecords) { + record.mExecutor.execute( + () -> + record.mDeviceSuggestionsCallback.onSuggestionUpdated( + suggestingPackageName, deviceSuggestions)); + } + } + private void notifyTransfer(RoutingController oldController, RoutingController newController) { for (TransferCallbackRecord record : mTransferCallbackRecords) { record.mExecutor.execute( @@ -1568,6 +1652,25 @@ public final class MediaRouter2 { .build(); } + /** + * Callback for receiving events about device suggestions + * + * @hide + */ + public interface DeviceSuggestionsCallback { + + /** + * Called when suggestions are updated. Whenever you register a callback, this will be + * invoked with the current suggestions. + * + * @param suggestingPackageName the package that provided the suggestions. + * @param suggestedDeviceInfo the suggestions provided by the package. + */ + void onSuggestionUpdated( + @NonNull String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + } + /** Callback for receiving events about media route discovery. */ public abstract static class RouteCallback { /** @@ -2326,6 +2429,35 @@ public final class MediaRouter2 { } } + private static final class DeviceSuggestionsCallbackRecord { + public final Executor mExecutor; + public final DeviceSuggestionsCallback mDeviceSuggestionsCallback; + + /* package */ DeviceSuggestionsCallbackRecord( + @NonNull Executor executor, + @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) { + mExecutor = executor; + mDeviceSuggestionsCallback = deviceSuggestionsCallback; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof DeviceSuggestionsCallbackRecord)) { + return false; + } + return mDeviceSuggestionsCallback + == ((DeviceSuggestionsCallbackRecord) obj).mDeviceSuggestionsCallback; + } + + @Override + public int hashCode() { + return mDeviceSuggestionsCallback.hashCode(); + } + } + static final class TransferCallbackRecord { public final Executor mExecutor; public final TransferCallback mTransferCallback; @@ -2446,6 +2578,17 @@ public final class MediaRouter2 { } @Override + public void notifyDeviceSuggestionsUpdated( + String suggestingPackageName, List<SuggestedDeviceInfo> suggestions) { + mHandler.sendMessage( + obtainMessage( + MediaRouter2::notifyDeviceSuggestionsUpdated, + MediaRouter2.this, + suggestingPackageName, + suggestions)); + } + + @Override public void requestCreateSessionByManager( long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) { mHandler.sendMessage( @@ -2487,6 +2630,11 @@ public final class MediaRouter2 { void setRouteListingPreference(@Nullable RouteListingPreference preference); + void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo); + + @Nullable + Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions(); + boolean showSystemOutputSwitcher(); List<MediaRoute2Info> getAllRoutes(); @@ -2687,6 +2835,29 @@ public final class MediaRouter2 { } @Override + public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + synchronized (mLock) { + try { + mMediaRouterService.setDeviceSuggestionsWithManager( + mClient, suggestedDeviceInfo); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + } + } + + @Override + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() { + synchronized (mLock) { + try { + return mMediaRouterService.getDeviceSuggestionsWithManager(mClient); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + } + + @Override public boolean showSystemOutputSwitcher() { try { return mMediaRouterService.showMediaOutputSwitcherWithProxyRouter(mClient); @@ -3296,6 +3467,23 @@ public final class MediaRouter2 { notifyRouteListingPreferenceUpdated(routeListingPreference); } + private void onDeviceSuggestionsChangeHandler( + @NonNull String packageName, + @NonNull String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + if (!TextUtils.equals(getClientPackageName(), packageName)) { + return; + } + synchronized (mLock) { + if (Objects.equals( + mSuggestedDeviceInfo.get(suggestingPackageName), suggestedDeviceInfo)) { + return; + } + mSuggestedDeviceInfo.put(suggestingPackageName, suggestedDeviceInfo); + } + notifyDeviceSuggestionsUpdated(suggestingPackageName, suggestedDeviceInfo); + } + private void onRequestFailedOnHandler(int requestId, int reason) { MediaRouter2Manager.TransferRequest matchingRequest = null; for (MediaRouter2Manager.TransferRequest request : mTransferRequests) { @@ -3390,6 +3578,20 @@ public final class MediaRouter2 { } @Override + public void notifyDeviceSuggestionsUpdated( + String packageName, + String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> deviceSuggestions) { + mHandler.sendMessage( + obtainMessage( + ProxyMediaRouter2Impl::onDeviceSuggestionsChangeHandler, + ProxyMediaRouter2Impl.this, + packageName, + suggestingPackageName, + deviceSuggestions)); + } + + @Override public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { mHandler.sendMessage( obtainMessage( @@ -3553,6 +3755,30 @@ public final class MediaRouter2 { } @Override + public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> deviceSuggestions) { + synchronized (mLock) { + try { + registerRouterStubIfNeededLocked(); + mMediaRouterService.setDeviceSuggestionsWithRouter2(mStub, deviceSuggestions); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + } + } + + @Override + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() { + synchronized (mLock) { + try { + return mMediaRouterService.getDeviceSuggestionsWithRouter2(mStub); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + } + + @Override public boolean showSystemOutputSwitcher() { synchronized (mLock) { try { diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 3f18eef2f9aa..bf88709eec33 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -1138,6 +1138,14 @@ public final class MediaRouter2Manager { } @Override + public void notifyDeviceSuggestionsUpdated( + String packageName, + String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + // MediaRouter2Manager doesn't support device suggestions + } + + @Override public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { mHandler.sendMessage( obtainMessage( diff --git a/media/java/android/media/SuggestedDeviceInfo.aidl b/media/java/android/media/SuggestedDeviceInfo.aidl new file mode 100644 index 000000000000..eab642572ed2 --- /dev/null +++ b/media/java/android/media/SuggestedDeviceInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2025 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 SuggestedDeviceInfo; diff --git a/media/java/android/media/SuggestedDeviceInfo.java b/media/java/android/media/SuggestedDeviceInfo.java new file mode 100644 index 000000000000..2aa139fcca17 --- /dev/null +++ b/media/java/android/media/SuggestedDeviceInfo.java @@ -0,0 +1,235 @@ +/* + * Copyright 2025 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 static com.android.media.flags.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.Objects; + +/** + * Allows applications to suggest routes to the system UI (for example, in the System UI Output + * Switcher). + * + * @see MediaRouter2#setSuggestedDevice + * @hide + */ +@FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API) +public final class SuggestedDeviceInfo implements Parcelable { + @NonNull + public static final Creator<SuggestedDeviceInfo> CREATOR = + new Creator<>() { + @Override + public SuggestedDeviceInfo createFromParcel(Parcel in) { + return new SuggestedDeviceInfo(in); + } + + @Override + public SuggestedDeviceInfo[] newArray(int size) { + return new SuggestedDeviceInfo[size]; + } + }; + + @NonNull private final String mDeviceDisplayName; + + @NonNull private final String mRouteId; + + private final int mType; + + @NonNull private final Bundle mExtras; + + private SuggestedDeviceInfo(Builder builder) { + mDeviceDisplayName = builder.mDeviceDisplayName; + mRouteId = builder.mRouteId; + mType = builder.mType; + mExtras = builder.mExtras; + } + + private SuggestedDeviceInfo(Parcel in) { + mDeviceDisplayName = in.readString(); + mRouteId = in.readString(); + mType = in.readInt(); + mExtras = in.readBundle(); + } + + /** + * Returns the name to be displayed to the user. + * + * @return The device display name. + */ + @NonNull + public String getDeviceDisplayName() { + return mDeviceDisplayName; + } + + /** + * Returns the route ID associated with the suggestion. + * + * @return The route ID. + */ + @NonNull + public String getRouteId() { + return mRouteId; + } + + /** + * Returns the device type associated with the suggestion. + * + * @return The device type. + */ + public int getType() { + return mType; + } + + /** + * Returns the extras associated with the suggestion. + * + * @return The extras. + */ + @Nullable + public Bundle getExtras() { + return mExtras; + } + + // SuggestedDeviceInfo Parcelable implementation. + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mDeviceDisplayName); + dest.writeString(mRouteId); + dest.writeInt(mType); + dest.writeBundle(mExtras); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof SuggestedDeviceInfo)) { + return false; + } + return Objects.equals(mDeviceDisplayName, ((SuggestedDeviceInfo) obj).mDeviceDisplayName) + && Objects.equals(mRouteId, ((SuggestedDeviceInfo) obj).mRouteId) + && mType == ((SuggestedDeviceInfo) obj).mType; + } + + @Override + public int hashCode() { + return Objects.hash(mDeviceDisplayName, mRouteId, mType); + } + + @Override + public String toString() { + return mDeviceDisplayName + " | " + mRouteId + " | " + mType; + } + + /** Builder for {@link SuggestedDeviceInfo}. */ + public static final class Builder { + @NonNull private String mDeviceDisplayName; + + @NonNull private String mRouteId; + + @NonNull private Integer mType; + + private Bundle mExtras = Bundle.EMPTY; + + /** + * Creates a new SuggestedDeviceInfo. The device display name, route ID, and type must be + * set. The extras cannot be null, but default to an empty {@link Bundle}. + * + * @throws IllegalArgumentException if the builder has a mandatory unset field. + */ + public SuggestedDeviceInfo build() { + if (mDeviceDisplayName == null) { + throw new IllegalArgumentException("Device display name cannot be null"); + } + + if (mRouteId == null) { + throw new IllegalArgumentException("Route ID cannot be null."); + } + + if (mType == null) { + throw new IllegalArgumentException("Device type cannot be null."); + } + + if (mExtras == null) { + throw new IllegalArgumentException("Extras cannot be null."); + } + + return new SuggestedDeviceInfo(this); + } + + /** + * Sets the {@link #getDeviceDisplayName() device display name}. + * + * @throws IllegalArgumentException if the name is null or empty. + */ + public Builder setDeviceDisplayName(@NonNull String deviceDisplayName) { + if (TextUtils.isEmpty(deviceDisplayName)) { + throw new IllegalArgumentException("Device display name cannot be null"); + } + mDeviceDisplayName = deviceDisplayName; + return this; + } + + /** + * Sets the {@link #getRouteId() route id}. + * + * @throws IllegalArgumentException if the route id is null or empty. + */ + public Builder setRouteId(@NonNull String routeId) { + if (TextUtils.isEmpty(routeId)) { + throw new IllegalArgumentException("Device display name cannot be null"); + } + mRouteId = routeId; + return this; + } + + /** Sets the {@link #getType() device type}. */ + public Builder setType(int type) { + mType = type; + return this; + } + + /** + * Sets the {@link #getExtras() extras}. + * + * <p>The default value is an empty {@link Bundle}. + * + * <p>Do not mutate the given {@link Bundle} after passing it to this method. You can use + * {@link Bundle#deepCopy()} to keep a mutable copy. + * + * @throws NullPointerException if the extras are null. + */ + public Builder setExtras(@NonNull Bundle extras) { + mExtras = Objects.requireNonNull(extras, "extras must not be null"); + return this; + } + } +} diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 0deed3982d9b..e39a0aa8717e 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -62,6 +62,14 @@ flag { } flag { + name: "enable_suggested_device_api" + is_exported: true + namespace: "media_better_together" + description: "Enables the API allowing proxy routers to suggest routes." + bug: "393216553" +} + +flag { name: "enable_full_scan_with_media_content_control" namespace: "media_better_together" description: "Allows holders of the MEDIA_CONTENT_CONTROL permission to scan for routes while not in the foreground." diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index f137de1b3e1d..988924d9f498 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -25,6 +25,7 @@ import static android.media.MediaRouter2.SCANNING_STATE_SCANNING_FULL; import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE; import static android.media.MediaRouter2Utils.getOriginalId; import static android.media.MediaRouter2Utils.getProviderId; + import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION; import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_DESELECT_ROUTE; @@ -63,6 +64,7 @@ import android.media.MediaRouter2Manager; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -76,18 +78,21 @@ import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.util.SparseArray; + import com.android.internal.annotations.GuardedBy; import com.android.internal.util.function.pooled.PooledLambda; import com.android.media.flags.Flags; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.statusbar.StatusBarManagerInternal; + import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -551,6 +556,36 @@ class MediaRouter2ServiceImpl { } } + public void setDeviceSuggestionsWithRouter2( + @NonNull IMediaRouter2 router, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + Objects.requireNonNull(router, "router must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + setDeviceSuggestionsWithRouter2Locked(router, suggestedDeviceInfo); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2( + @NonNull IMediaRouter2 router) { + Objects.requireNonNull(router, "router must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + return getDeviceSuggestionsWithRouter2Locked(router); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + // End of methods that implement MediaRouter2 operations. // Start of methods that implement MediaRouter2Manager operations. @@ -805,6 +840,36 @@ class MediaRouter2ServiceImpl { } } + public void setDeviceSuggestionsWithManager( + @NonNull IMediaRouter2Manager manager, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + Objects.requireNonNull(manager, "manager must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + setDeviceSuggestionsWithManagerLocked(manager, suggestedDeviceInfo); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager( + @NonNull IMediaRouter2Manager manager) { + Objects.requireNonNull(manager, "manager must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + return getDeviceSuggestionsWithManagerLocked(manager); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) public boolean showMediaOutputSwitcherWithProxyRouter( @NonNull IMediaRouter2Manager proxyRouter) { @@ -1582,6 +1647,61 @@ class MediaRouter2ServiceImpl { DUMMY_REQUEST_ID, routerRecord, uniqueSessionId)); } + @GuardedBy("mLock") + private void setDeviceSuggestionsWithRouter2Locked( + @NonNull IMediaRouter2 router, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + final IBinder binder = router.asBinder(); + final RouterRecord routerRecord = mAllRouterRecords.get(binder); + + if (routerRecord == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Ignoring set device suggestion for unknown router: %s", router)); + return; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "setDeviceSuggestions | router: %d suggestion: %d", + routerRecord.mPackageName, suggestedDeviceInfo)); + + routerRecord.mUserRecord.updateDeviceSuggestionsLocked( + routerRecord.mPackageName, routerRecord.mPackageName, suggestedDeviceInfo); + routerRecord.mUserRecord.mHandler.sendMessage( + obtainMessage( + UserHandler::notifyDeviceSuggestionsUpdatedOnHandler, + routerRecord.mUserRecord.mHandler, + routerRecord.mPackageName, + routerRecord.mPackageName, + suggestedDeviceInfo)); + } + + @GuardedBy("mLock") + @Nullable + private Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2Locked( + @NonNull IMediaRouter2 router) { + final IBinder binder = router.asBinder(); + final RouterRecord routerRecord = mAllRouterRecords.get(binder); + + if (routerRecord == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Attempted to get device suggestion for unknown router: %s", router)); + return null; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "getDeviceSuggestions | router: %d", routerRecord.mPackageName)); + + return routerRecord.mUserRecord.getDeviceSuggestionsLocked(routerRecord.mPackageName); + } + // End of locked methods that are used by MediaRouter2. // Start of locked methods that are used by MediaRouter2Manager. @@ -1972,6 +2092,68 @@ class MediaRouter2ServiceImpl { uniqueRequestId, routerRecord, uniqueSessionId)); } + @GuardedBy("mLock") + private void setDeviceSuggestionsWithManagerLocked( + @NonNull IMediaRouter2Manager manager, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + final IBinder binder = manager.asBinder(); + ManagerRecord managerRecord = mAllManagerRecords.get(binder); + + if (managerRecord == null || managerRecord.mTargetPackageName == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Ignoring set device suggestion for unknown manager: %s", manager)); + return; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "setDeviceSuggestions | manager: %d, suggestingPackageName: %d suggestion:" + + " %d", + managerRecord.mManagerId, + managerRecord.mOwnerPackageName, + suggestedDeviceInfo)); + + managerRecord.mUserRecord.updateDeviceSuggestionsLocked( + managerRecord.mTargetPackageName, + managerRecord.mOwnerPackageName, + suggestedDeviceInfo); + managerRecord.mUserRecord.mHandler.sendMessage( + obtainMessage( + UserHandler::notifyDeviceSuggestionsUpdatedOnHandler, + managerRecord.mUserRecord.mHandler, + managerRecord.mTargetPackageName, + managerRecord.mOwnerPackageName, + suggestedDeviceInfo)); + } + + @GuardedBy("mLock") + @Nullable + private Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManagerLocked( + @NonNull IMediaRouter2Manager manager) { + final IBinder binder = manager.asBinder(); + ManagerRecord managerRecord = mAllManagerRecords.get(binder); + + if (managerRecord == null || managerRecord.mTargetPackageName == null) { + Slog.w( + TAG, + TextUtils.formatSimple( + "Attempted to get device suggestion for unknown manager: %s", manager)); + return null; + } + + Slog.i( + TAG, + TextUtils.formatSimple( + "getDeviceSuggestionsWithManagerLocked | manager: %d", + managerRecord.mManagerId)); + + return managerRecord.mUserRecord.getDeviceSuggestionsLocked( + managerRecord.mTargetPackageName); + } + // End of locked methods that are used by MediaRouter2Manager. // Start of locked methods that are used by both MediaRouter2 and MediaRouter2Manager. @@ -2047,6 +2229,11 @@ class MediaRouter2ServiceImpl { //TODO: make records private for thread-safety final ArrayList<RouterRecord> mRouterRecords = new ArrayList<>(); final ArrayList<ManagerRecord> mManagerRecords = new ArrayList<>(); + + // @GuardedBy("mLock") + private final Map<String, Map<String, List<SuggestedDeviceInfo>>> mDeviceSuggestions = + new HashMap<>(); + RouteDiscoveryPreference mCompositeDiscoveryPreference = RouteDiscoveryPreference.EMPTY; Set<String> mActivelyScanningPackages = Set.of(); final UserHandler mHandler; @@ -2076,6 +2263,25 @@ class MediaRouter2ServiceImpl { return null; } + // @GuardedBy("mLock") + public void updateDeviceSuggestionsLocked( + String packageName, + String suggestingPackageName, + List<SuggestedDeviceInfo> deviceSuggestions) { + mDeviceSuggestions.putIfAbsent( + packageName, new HashMap<String, List<SuggestedDeviceInfo>>()); + Map<String, List<SuggestedDeviceInfo>> suggestions = + mDeviceSuggestions.get(packageName); + suggestions.put(suggestingPackageName, deviceSuggestions); + } + + // @GuardedBy("mLock") + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsLocked( + String packageName) { + return mDeviceSuggestions.get(packageName); + } + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { pw.println(prefix + "UserRecord"); @@ -2314,6 +2520,15 @@ class MediaRouter2ServiceImpl { } } + public void notifyDeviceSuggestionsUpdated( + String suggestingPackageName, List<SuggestedDeviceInfo> suggestedDeviceInfo) { + try { + mRouter.notifyDeviceSuggestionsUpdated(suggestingPackageName, suggestedDeviceInfo); + } catch (RemoteException ex) { + logRemoteException("notifyDeviceSuggestionsUpdated", ex); + } + } + /** * Sends the corresponding router a {@link RoutingSessionInfo session} creation request, * with the given {@link MediaRoute2Info} as the initial member. @@ -3556,6 +3771,41 @@ class MediaRouter2ServiceImpl { // need to update routers other than the one making the update. } + private void notifyDeviceSuggestionsUpdatedOnHandler( + String routerPackageName, + String suggestingPackageName, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + MediaRouter2ServiceImpl service = mServiceRef.get(); + if (service == null) { + return; + } + List<IMediaRouter2Manager> managers = new ArrayList<>(); + synchronized (service.mLock) { + for (ManagerRecord managerRecord : mUserRecord.mManagerRecords) { + if (TextUtils.equals(managerRecord.mTargetPackageName, routerPackageName)) { + managers.add(managerRecord.mManager); + } + } + for (IMediaRouter2Manager manager : managers) { + try { + manager.notifyDeviceSuggestionsUpdated( + routerPackageName, suggestingPackageName, suggestedDeviceInfo); + } catch (RemoteException ex) { + Slog.w( + TAG, + "Failed to notify suggesteion changed. Manager probably died.", + ex); + } + } + for (RouterRecord routerRecord : mUserRecord.mRouterRecords) { + if (TextUtils.equals(routerRecord.mPackageName, routerPackageName)) { + routerRecord.notifyDeviceSuggestionsUpdated( + suggestingPackageName, suggestedDeviceInfo); + } + } + } + } + private void updateDiscoveryPreferenceOnHandler() { MediaRouter2ServiceImpl service = mServiceRef.get(); if (service == null) { diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 35bb19943a24..11f449e790a8 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -49,6 +49,7 @@ import android.media.RemoteDisplayState.RemoteDisplayInfo; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.media.SuggestedDeviceInfo; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -80,6 +81,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -526,6 +528,21 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override + public void setDeviceSuggestionsWithRouter2( + IMediaRouter2 router, @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + mService2.setDeviceSuggestionsWithRouter2(router, suggestedDeviceInfo); + } + + // Binder call + @Override + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2( + IMediaRouter2 router) { + return mService2.getDeviceSuggestionsWithRouter2(router); + } + + // Binder call + @Override public List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager) { return mService2.getRemoteSessions(manager); } @@ -666,6 +683,22 @@ public final class MediaRouterService extends IMediaRouterService.Stub return mService2.showMediaOutputSwitcherWithProxyRouter(proxyRouter); } + // Binder call + @Override + public void setDeviceSuggestionsWithManager( + @NonNull IMediaRouter2Manager manager, + @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) { + mService2.setDeviceSuggestionsWithManager(manager, suggestedDeviceInfo); + } + + // Binder call + @Override + @Nullable + public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager( + IMediaRouter2Manager manager) { + return mService2.getDeviceSuggestionsWithManager(manager); + } + void restoreBluetoothA2dp() { try { boolean a2dpOn; |