summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--media/java/android/media/IMediaRouter2.aidl3
-rw-r--r--media/java/android/media/IMediaRouter2Manager.aidl3
-rw-r--r--media/java/android/media/IMediaRouterService.aidl9
-rw-r--r--media/java/android/media/MediaRouter2.java226
-rw-r--r--media/java/android/media/MediaRouter2Manager.java8
-rw-r--r--media/java/android/media/SuggestedDeviceInfo.aidl19
-rw-r--r--media/java/android/media/SuggestedDeviceInfo.java235
-rw-r--r--media/java/android/media/flags/media_better_together.aconfig8
-rw-r--r--services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java250
-rw-r--r--services/core/java/com/android/server/media/MediaRouterService.java33
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;