diff options
20 files changed, 1075 insertions, 199 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index ace00fcb58aa..ecb242a6a217 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -23972,6 +23972,7 @@ package android.media { method @Nullable public android.net.Uri getIconUri(); method @NonNull public String getId(); method @NonNull public CharSequence getName(); + method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public int getSuitabilityStatus(); method public int getType(); method public int getVolume(); method public int getVolumeHandling(); @@ -23989,6 +23990,9 @@ package android.media { field public static final String FEATURE_REMOTE_VIDEO_PLAYBACK = "android.media.route.feature.REMOTE_VIDEO_PLAYBACK"; field public static final int PLAYBACK_VOLUME_FIXED = 0; // 0x0 field public static final int PLAYBACK_VOLUME_VARIABLE = 1; // 0x1 + field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER = 2; // 0x2 + field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER = 0; // 0x0 + field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int SUITABILITY_STATUS_SUITABLE_FOR_MANUAL_TRANSFER = 1; // 0x1 field public static final int TYPE_BLE_HEADSET = 26; // 0x1a field public static final int TYPE_BLUETOOTH_A2DP = 8; // 0x8 field public static final int TYPE_BUILTIN_SPEAKER = 2; // 0x2 @@ -24029,6 +24033,7 @@ package android.media { method @NonNull public android.media.MediaRoute2Info.Builder setDescription(@Nullable CharSequence); method @NonNull public android.media.MediaRoute2Info.Builder setExtras(@Nullable android.os.Bundle); method @NonNull public android.media.MediaRoute2Info.Builder setIconUri(@Nullable android.net.Uri); + method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") @NonNull public android.media.MediaRoute2Info.Builder setSuitabilityStatus(int); method @NonNull public android.media.MediaRoute2Info.Builder setType(int); method @NonNull public android.media.MediaRoute2Info.Builder setVisibilityPublic(); method @NonNull public android.media.MediaRoute2Info.Builder setVisibilityRestricted(@NonNull java.util.Set<java.lang.String>); @@ -24242,6 +24247,7 @@ package android.media { method public void release(); method public void selectRoute(@NonNull android.media.MediaRoute2Info); method public void setVolume(int); + method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public boolean wasTransferRequestedBySelf(); } public abstract static class MediaRouter2.TransferCallback { @@ -24639,12 +24645,16 @@ package android.media { method @Nullable public CharSequence getName(); method @NonNull public java.util.List<java.lang.String> getSelectableRoutes(); method @NonNull public java.util.List<java.lang.String> getSelectedRoutes(); + method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public int getTransferReason(); method @NonNull public java.util.List<java.lang.String> getTransferableRoutes(); method public int getVolume(); method public int getVolumeHandling(); method public int getVolumeMax(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.media.RoutingSessionInfo> CREATOR; + field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int TRANSFER_REASON_APP = 2; // 0x2 + field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int TRANSFER_REASON_FALLBACK = 0; // 0x0 + field @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public static final int TRANSFER_REASON_SYSTEM_REQUEST = 1; // 0x1 } public static final class RoutingSessionInfo.Builder { @@ -24665,6 +24675,8 @@ package android.media { method @NonNull public android.media.RoutingSessionInfo.Builder removeTransferableRoute(@NonNull String); method @NonNull public android.media.RoutingSessionInfo.Builder setControlHints(@Nullable android.os.Bundle); method @NonNull public android.media.RoutingSessionInfo.Builder setName(@Nullable CharSequence); + method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") @NonNull public android.media.RoutingSessionInfo.Builder setTransferInitiator(@Nullable android.os.UserHandle, @Nullable String); + method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") @NonNull public android.media.RoutingSessionInfo.Builder setTransferReason(int); method @NonNull public android.media.RoutingSessionInfo.Builder setVolume(int); method @NonNull public android.media.RoutingSessionInfo.Builder setVolumeHandling(int); method @NonNull public android.media.RoutingSessionInfo.Builder setVolumeMax(int); diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index d0de5f0e4104..5400c5848f02 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6862,4 +6862,8 @@ <!-- Whether the media player is shown on the quick settings --> <bool name="config_quickSettingsShowMediaPlayer">true</bool> + + <!-- Defines suitability of the built-in speaker route. + Refer to {@link MediaRoute2Info} to see supported values. --> + <integer name="config_mediaRouter_builtInSpeakerSuitability">0</integer> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 38943306b962..eeef19291c4f 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5306,4 +5306,7 @@ <java-symbol type="bool" name="config_viewBasedRotaryEncoderHapticsEnabled" /> <java-symbol type="bool" name="config_quickSettingsShowMediaPlayer" /> + + <!-- Android MediaRouter framework configs. --> + <java-symbol type="integer" name="config_mediaRouter_builtInSpeakerSuitability" /> </resources> diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl index 29bfd1acae17..e2dddad274e1 100644 --- a/media/java/android/media/IMediaRouter2.aidl +++ b/media/java/android/media/IMediaRouter2.aidl @@ -19,6 +19,7 @@ package android.media; import android.media.MediaRoute2Info; import android.media.RoutingSessionInfo; import android.os.Bundle; +import android.os.UserHandle; /** * @hide @@ -35,5 +36,6 @@ oneway interface IMediaRouter2 { * Call MediaRouterService#requestCreateSessionWithRouter2 to pass the result. */ void requestCreateSessionByManager(long uniqueRequestId, in RoutingSessionInfo oldSession, - in MediaRoute2Info route); + in MediaRoute2Info route, in UserHandle transferInitiatorUserHandle, + in String transferInitiatorPackageName); } diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index fa4d1a1ff935..04e99ea8a57f 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -64,7 +64,8 @@ interface IMediaRouterService { void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId, long managerRequestId, in RoutingSessionInfo oldSession, in MediaRoute2Info route, - in @nullable Bundle sessionHints); + in @nullable Bundle sessionHints, in UserHandle transferInitiatorUserHandle, + in String transferInitiatorPackageName); void selectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route); void deselectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route); void transferToRouteWithRouter2(IMediaRouter2 router, String sessionId, @@ -84,13 +85,16 @@ interface IMediaRouterService { void stopScan(IMediaRouter2Manager manager); void requestCreateSessionWithManager(IMediaRouter2Manager manager, int requestId, - in RoutingSessionInfo oldSession, in @nullable MediaRoute2Info route); + in RoutingSessionInfo oldSession, in @nullable MediaRoute2Info route, + in UserHandle transferInitiatorUserHandle, in String transferInitiatorPackageName); void selectRouteWithManager(IMediaRouter2Manager manager, int requestId, String sessionId, in MediaRoute2Info route); void deselectRouteWithManager(IMediaRouter2Manager manager, int requestId, String sessionId, in MediaRoute2Info route); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL)") void transferToRouteWithManager(IMediaRouter2Manager manager, int requestId, - String sessionId, in MediaRoute2Info route); + String sessionId, in MediaRoute2Info route, + in UserHandle transferInitiatorUserHandle, String transferInitiatorPackageName); void setSessionVolumeWithManager(IMediaRouter2Manager manager, int requestId, String sessionId, int volume); void releaseSessionWithManager(IMediaRouter2Manager manager, int requestId, String sessionId); diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index 8ad35876989d..0eabe66e9a69 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -19,6 +19,7 @@ package android.media; import static android.media.MediaRouter2Utils.toUniqueId; import static com.android.media.flags.Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER; +import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES; import static com.android.media.flags.Flags.FLAG_ENABLE_NEW_MEDIA_ROUTE_2_INFO_TYPES; import android.annotation.FlaggedApi; @@ -479,6 +480,37 @@ public final class MediaRoute2Info implements Parcelable { public static final String FEATURE_REMOTE_GROUP_PLAYBACK = "android.media.route.feature.REMOTE_GROUP_PLAYBACK"; + /** Indicates the route is always suitable for media playback. */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public static final int SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER = 0; + + /** + * Indicates that the route is suitable for media playback only after explicit user selection. + */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public static final int SUITABILITY_STATUS_SUITABLE_FOR_MANUAL_TRANSFER = 1; + + /** Indicates that the route is never suitable for media playback. */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public static final int SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER = 2; + + /** + * Route suitability status. + * + * <p>Signals whether the route is suitable to play media. + * + * @hide + */ + @IntDef( + value = { + SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER, + SUITABILITY_STATUS_SUITABLE_FOR_MANUAL_TRANSFER, + SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER + }) + @Retention(RetentionPolicy.SOURCE) + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public @interface SuitabilityStatus {} + private final String mId; private final CharSequence mName; private final List<String> mFeatures; @@ -500,6 +532,7 @@ public final class MediaRoute2Info implements Parcelable { private final String mProviderId; private final boolean mIsVisibilityRestricted; private final Set<String> mAllowedPackages; + @SuitabilityStatus private final int mSuitabilityStatus; MediaRoute2Info(@NonNull Builder builder) { mId = builder.mId; @@ -521,6 +554,7 @@ public final class MediaRoute2Info implements Parcelable { mProviderId = builder.mProviderId; mIsVisibilityRestricted = builder.mIsVisibilityRestricted; mAllowedPackages = builder.mAllowedPackages; + mSuitabilityStatus = builder.mSuitabilityStatus; } MediaRoute2Info(@NonNull Parcel in) { @@ -544,6 +578,7 @@ public final class MediaRoute2Info implements Parcelable { mProviderId = in.readString(); mIsVisibilityRestricted = in.readBoolean(); mAllowedPackages = Set.of(in.createString8Array()); + mSuitabilityStatus = in.readInt(); } /** @@ -778,6 +813,13 @@ public final class MediaRoute2Info implements Parcelable { || mAllowedPackages.contains(packageName); } + /** Returns the route suitability status. */ + @SuitabilityStatus + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public int getSuitabilityStatus() { + return mSuitabilityStatus; + } + /** * Dumps the current state of the object to the given {@code pw} as a human-readable string. * @@ -809,6 +851,7 @@ public final class MediaRoute2Info implements Parcelable { pw.println(indent + "mProviderId=" + mProviderId); pw.println(indent + "mIsVisibilityRestricted=" + mIsVisibilityRestricted); pw.println(indent + "mAllowedPackages=" + mAllowedPackages); + pw.println(indent + "mSuitabilityStatus=" + mSuitabilityStatus); } private void dumpVolume(@NonNull PrintWriter pw, @NonNull String prefix) { @@ -861,39 +904,74 @@ public final class MediaRoute2Info implements Parcelable { && Objects.equals(mDeduplicationIds, other.mDeduplicationIds) && Objects.equals(mProviderId, other.mProviderId) && (mIsVisibilityRestricted == other.mIsVisibilityRestricted) - && Objects.equals(mAllowedPackages, other.mAllowedPackages); + && Objects.equals(mAllowedPackages, other.mAllowedPackages) + && mSuitabilityStatus == other.mSuitabilityStatus; } @Override public int hashCode() { // Note: mExtras is not included. - return Objects.hash(mId, mName, mFeatures, mType, mIsSystem, mIconUri, mDescription, - mConnectionState, mClientPackageName, mPackageName, mVolumeHandling, mVolumeMax, - mVolume, mAddress, mDeduplicationIds, mProviderId, mIsVisibilityRestricted, - mAllowedPackages); + return Objects.hash( + mId, + mName, + mFeatures, + mType, + mIsSystem, + mIconUri, + mDescription, + mConnectionState, + mClientPackageName, + mPackageName, + mVolumeHandling, + mVolumeMax, + mVolume, + mAddress, + mDeduplicationIds, + mProviderId, + mIsVisibilityRestricted, + mAllowedPackages, + mSuitabilityStatus); } @Override public String toString() { // Note: mExtras is not printed here. - StringBuilder result = new StringBuilder() - .append("MediaRoute2Info{ ") - .append("id=").append(getId()) - .append(", name=").append(getName()) - .append(", features=").append(getFeatures()) - .append(", iconUri=").append(getIconUri()) - .append(", description=").append(getDescription()) - .append(", connectionState=").append(getConnectionState()) - .append(", clientPackageName=").append(getClientPackageName()) - .append(", volumeHandling=").append(getVolumeHandling()) - .append(", volumeMax=").append(getVolumeMax()) - .append(", volume=").append(getVolume()) - .append(", address=").append(getAddress()) - .append(", deduplicationIds=").append(String.join(",", getDeduplicationIds())) - .append(", providerId=").append(getProviderId()) - .append(", isVisibilityRestricted=").append(mIsVisibilityRestricted) - .append(", allowedPackages=").append(String.join(",", mAllowedPackages)) - .append(" }"); + StringBuilder result = + new StringBuilder() + .append("MediaRoute2Info{ ") + .append("id=") + .append(getId()) + .append(", name=") + .append(getName()) + .append(", features=") + .append(getFeatures()) + .append(", iconUri=") + .append(getIconUri()) + .append(", description=") + .append(getDescription()) + .append(", connectionState=") + .append(getConnectionState()) + .append(", clientPackageName=") + .append(getClientPackageName()) + .append(", volumeHandling=") + .append(getVolumeHandling()) + .append(", volumeMax=") + .append(getVolumeMax()) + .append(", volume=") + .append(getVolume()) + .append(", address=") + .append(getAddress()) + .append(", deduplicationIds=") + .append(String.join(",", getDeduplicationIds())) + .append(", providerId=") + .append(getProviderId()) + .append(", isVisibilityRestricted=") + .append(mIsVisibilityRestricted) + .append(", allowedPackages=") + .append(String.join(",", mAllowedPackages)) + .append(", suitabilityStatus=") + .append(mSuitabilityStatus) + .append(" }"); return result.toString(); } @@ -923,6 +1001,7 @@ public final class MediaRoute2Info implements Parcelable { dest.writeString(mProviderId); dest.writeBoolean(mIsVisibilityRestricted); dest.writeString8Array(mAllowedPackages.toArray(new String[0])); + dest.writeInt(mSuitabilityStatus); } private static String getDeviceTypeString(@Type int deviceType) { @@ -1005,6 +1084,7 @@ public final class MediaRoute2Info implements Parcelable { private String mProviderId; private boolean mIsVisibilityRestricted; private Set<String> mAllowedPackages; + @SuitabilityStatus private int mSuitabilityStatus; /** * Constructor for builder to create {@link MediaRoute2Info}. @@ -1028,6 +1108,7 @@ public final class MediaRoute2Info implements Parcelable { mFeatures = new ArrayList<>(); mDeduplicationIds = Set.of(); mAllowedPackages = Set.of(); + mSuitabilityStatus = SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER; } /** @@ -1075,6 +1156,7 @@ public final class MediaRoute2Info implements Parcelable { mProviderId = routeInfo.mProviderId; mIsVisibilityRestricted = routeInfo.mIsVisibilityRestricted; mAllowedPackages = routeInfo.mAllowedPackages; + mSuitabilityStatus = routeInfo.mSuitabilityStatus; } /** @@ -1318,6 +1400,23 @@ public final class MediaRoute2Info implements Parcelable { } /** + * Sets route suitability status. + * + * <p>The default value is {@link + * MediaRoute2Info#SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER}. + * + * <p> Apps are not supposed to set {@link + * MediaRoute2Info#SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER}. Publishing a non-system + * route with such status throws {@link SecurityException}. + */ + @NonNull + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public Builder setSuitabilityStatus(@SuitabilityStatus int suitabilityStatus) { + mSuitabilityStatus = suitabilityStatus; + return this; + } + + /** * Builds the {@link MediaRoute2Info media route info}. * * @throws IllegalArgumentException if no features are added. diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index ba26df922f23..5e235515c852 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -17,6 +17,7 @@ package android.media; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; +import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES; import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2; import static com.android.media.flags.Flags.FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2; @@ -699,15 +700,48 @@ public final class MediaRouter2 { * @hide */ @SystemApi - @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) + @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) { - mImpl.transfer(controller.getRoutingSessionInfo(), route); + mImpl.transfer( + controller.getRoutingSessionInfo(), + route, + android.os.Process.myUserHandle(), + mContext.getPackageName()); + } + + /** + * Transfers the media of a routing controller to the given route. + * + * <p>This will be no-op for non-system media routers. + * + * @param controller a routing controller controlling media routing. + * @param route the route you want to transfer the media to. + * @param transferInitiatorUserHandle the user handle of the app that initiated the transfer + * request. + * @param transferInitiatorPackageName the package name of the app that initiated the transfer. + * This value is used with the user handle to populate {@link + * RoutingController#wasTransferRequestedBySelf()}. + * @hide + */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public void transfer( + @NonNull RoutingController controller, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { + mImpl.transfer( + controller.getRoutingSessionInfo(), + route, + transferInitiatorUserHandle, + transferInitiatorPackageName); } void requestCreateController( @NonNull RoutingController controller, @NonNull MediaRoute2Info route, - long managerRequestId) { + long managerRequestId, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { final int requestId = mNextRequestId.getAndIncrement(); @@ -736,7 +770,9 @@ public final class MediaRouter2 { managerRequestId, controller.getRoutingSessionInfo(), route, - controllerHints); + controllerHints, + transferInitiatorUserHandle, + transferInitiatorPackageName); } catch (RemoteException ex) { Log.e(TAG, "createControllerForTransfer: " + "Failed to request for creating a controller.", ex); @@ -1053,7 +1089,11 @@ public final class MediaRouter2 { } void onRequestCreateControllerByManagerOnHandler( - RoutingSessionInfo oldSession, MediaRoute2Info route, long managerRequestId) { + RoutingSessionInfo oldSession, + MediaRoute2Info route, + long managerRequestId, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { Log.i( TAG, TextUtils.formatSimple( @@ -1070,7 +1110,8 @@ public final class MediaRouter2 { if (controller == null) { return; } - requestCreateController(controller, route, managerRequestId); + requestCreateController(controller, route, managerRequestId, transferInitiatorUserHandle, + transferInitiatorPackageName); } private List<MediaRoute2Info> getSortedRoutes( @@ -1469,6 +1510,21 @@ public final class MediaRouter2 { } /** + * Returns whether the transfer was requested by the calling app (as determined by comparing + * {@link UserHandle} and package name). + */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public boolean wasTransferRequestedBySelf() { + RoutingSessionInfo sessionInfo = getRoutingSessionInfo(); + + UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle(); + String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName(); + + return Objects.equals(android.os.Process.myUserHandle(), transferInitiatorUserHandle) + && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName); + } + + /** * Returns the current {@link RoutingSessionInfo} associated to this controller. */ @NonNull @@ -1980,14 +2036,20 @@ public final class MediaRouter2 { @Override public void requestCreateSessionByManager( - long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) { + long managerRequestId, + RoutingSessionInfo oldSession, + MediaRoute2Info route, + UserHandle transferInitiatorUserHandle, + String transferInitiatorPackageName) { mHandler.sendMessage( obtainMessage( MediaRouter2::onRequestCreateControllerByManagerOnHandler, MediaRouter2.this, oldSession, route, - managerRequestId)); + managerRequestId, + transferInitiatorUserHandle, + transferInitiatorPackageName)); } } @@ -2027,7 +2089,11 @@ public final class MediaRouter2 { void stop(); - void transfer(RoutingSessionInfo sessionInfo, MediaRoute2Info route); + void transfer( + @NonNull RoutingSessionInfo sessionInfo, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName); List<RoutingController> getControllers(); @@ -2220,7 +2286,11 @@ public final class MediaRouter2 { List<RoutingSessionInfo> sessionInfos = getRoutingSessions(); RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1); - transfer(targetSession, route); + transfer( + targetSession, + route, + android.os.Process.myUserHandle(), + mContext.getPackageName()); } @Override @@ -2243,14 +2313,24 @@ public final class MediaRouter2 { * * @param sessionInfo The {@link RoutingSessionInfo routing session} to transfer. * @param route The {@link MediaRoute2Info route} to transfer to. - * @see #transferToRoute(RoutingSessionInfo, MediaRoute2Info) + * @param transferInitiatorUserHandle The user handle of the app that initiated the + * transfer. + * @param transferInitiatorPackageName The package name if of the app that initiated the + * transfer. + * @see #transferToRoute(RoutingSessionInfo, MediaRoute2Info, UserHandle, String) * @see #requestCreateSession(RoutingSessionInfo, MediaRoute2Info) */ @Override + @SuppressWarnings("AndroidFrameworkRequiresPermission") public void transfer( - @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) { + @NonNull RoutingSessionInfo sessionInfo, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); Objects.requireNonNull(route, "route must not be null"); + Objects.requireNonNull(transferInitiatorUserHandle); + Objects.requireNonNull(transferInitiatorPackageName); Log.v( TAG, @@ -2268,9 +2348,14 @@ public final class MediaRouter2 { } if (sessionInfo.getTransferableRoutes().contains(route.getId())) { - transferToRoute(sessionInfo, route); + transferToRoute( + sessionInfo, + route, + transferInitiatorUserHandle, + transferInitiatorPackageName); } else { - requestCreateSession(sessionInfo, route); + requestCreateSession(sessionInfo, route, transferInitiatorUserHandle, + transferInitiatorPackageName); } } @@ -2282,21 +2367,30 @@ public final class MediaRouter2 { * RoutingSessionInfo routing session's} {@link RoutingSessionInfo#getTransferableRoutes() * transferable routes list}. Otherwise, the request will fail. * - * <p>Use {@link #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)} to request - * an out-of-session transfer. + * <p>Use {@link #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)} to request an + * out-of-session transfer. * * @param session The {@link RoutingSessionInfo routing session} to transfer. * @param route The {@link MediaRoute2Info route} to transfer to. Must be one of the {@link * RoutingSessionInfo routing session's} {@link * RoutingSessionInfo#getTransferableRoutes() transferable routes}. */ + @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) private void transferToRoute( - @NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) { + @NonNull RoutingSessionInfo session, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { int requestId = createTransferRequest(session, route); try { mMediaRouterService.transferToRouteWithManager( - mClient, requestId, session.getId(), route); + mClient, + requestId, + session.getId(), + route, + transferInitiatorUserHandle, + transferInitiatorPackageName); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } @@ -2317,7 +2411,10 @@ public final class MediaRouter2 { * @param route The {@link MediaRoute2Info route} to transfer to. */ private void requestCreateSession( - @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) { + @NonNull RoutingSessionInfo oldSession, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { if (TextUtils.isEmpty(oldSession.getClientPackageName())) { Log.w(TAG, "requestCreateSession: Can't create a session without package name."); this.onTransferFailed(oldSession, route); @@ -2328,7 +2425,12 @@ public final class MediaRouter2 { try { mMediaRouterService.requestCreateSessionWithManager( - mClient, requestId, oldSession, route); + mClient, + requestId, + oldSession, + route, + transferInitiatorUserHandle, + transferInitiatorPackageName); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } @@ -3055,7 +3157,8 @@ public final class MediaRouter2 { return; } - requestCreateController(controller, route, MANAGER_REQUEST_ID_NONE); + requestCreateController(controller, route, MANAGER_REQUEST_ID_NONE, + android.os.Process.myUserHandle(), mContext.getPackageName()); } @Override @@ -3071,7 +3174,11 @@ public final class MediaRouter2 { * #transferTo(MediaRoute2Info)}. */ @Override - public void transfer(RoutingSessionInfo sessionInfo, MediaRoute2Info route) { + public void transfer( + @NonNull RoutingSessionInfo sessionInfo, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { // Do nothing. } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 830708cb38b2..06c0996785c2 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -18,9 +18,11 @@ package android.media; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; +import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.content.Context; import android.media.session.MediaController; import android.media.session.MediaSessionManager; @@ -28,6 +30,7 @@ import android.os.Handler; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -467,30 +470,42 @@ public final class MediaRouter2Manager { * <p>Same as {@link #transfer(RoutingSessionInfo, MediaRoute2Info)}, but resolves the routing * session based on the provided package name. */ - public void transfer(@NonNull String packageName, @NonNull MediaRoute2Info route) { + @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) + public void transfer( + @NonNull String packageName, + @NonNull MediaRoute2Info route, + @NonNull UserHandle userHandle) { Objects.requireNonNull(packageName, "packageName must not be null"); Objects.requireNonNull(route, "route must not be null"); List<RoutingSessionInfo> sessionInfos = getRoutingSessions(packageName); RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1); - transfer(targetSession, route); + transfer(targetSession, route, userHandle, packageName); } /** * Transfers a routing session to a media route. + * * <p>{@link Callback#onTransferred} or {@link Callback#onTransferFailed} will be called * depending on the result. * * @param sessionInfo the routing session info to transfer * @param route the route transfer to - * + * @param transferInitiatorUserHandle the user handle of an app initiated the transfer + * @param transferInitiatorPackageName the package name of an app initiated the transfer * @see Callback#onTransferred(RoutingSessionInfo, RoutingSessionInfo) * @see Callback#onTransferFailed(RoutingSessionInfo, MediaRoute2Info) */ - public void transfer(@NonNull RoutingSessionInfo sessionInfo, - @NonNull MediaRoute2Info route) { + @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) + public void transfer( + @NonNull RoutingSessionInfo sessionInfo, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); Objects.requireNonNull(route, "route must not be null"); + Objects.requireNonNull(transferInitiatorUserHandle); + Objects.requireNonNull(transferInitiatorPackageName); Log.v(TAG, "Transferring routing session. session= " + sessionInfo + ", route=" + route); @@ -503,9 +518,11 @@ public final class MediaRouter2Manager { } if (sessionInfo.getTransferableRoutes().contains(route.getId())) { - transferToRoute(sessionInfo, route); + transferToRoute( + sessionInfo, route, transferInitiatorUserHandle, transferInitiatorPackageName); } else { - requestCreateSession(sessionInfo, route); + requestCreateSession(sessionInfo, route, transferInitiatorUserHandle, + transferInitiatorPackageName); } } @@ -873,19 +890,30 @@ public final class MediaRouter2Manager { * * @hide */ - private void transferToRoute(@NonNull RoutingSessionInfo session, - @NonNull MediaRoute2Info route) { + @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) + private void transferToRoute( + @NonNull RoutingSessionInfo session, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { int requestId = createTransferRequest(session, route); try { mMediaRouterService.transferToRouteWithManager( - mClient, requestId, session.getId(), route); + mClient, + requestId, + session.getId(), + route, + transferInitiatorUserHandle, + transferInitiatorPackageName); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } - private void requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route) { + private void requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiationPackageName) { if (TextUtils.isEmpty(oldSession.getClientPackageName())) { Log.w(TAG, "requestCreateSession: Can't create a session without package name."); notifyTransferFailed(oldSession, route); @@ -896,7 +924,8 @@ public final class MediaRouter2Manager { try { mMediaRouterService.requestCreateSessionWithManager( - mClient, requestId, oldSession, route); + mClient, requestId, oldSession, route, transferInitiatorUserHandle, + transferInitiationPackageName); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java index a77c9432f197..d28c26df6749 100644 --- a/media/java/android/media/RoutingSessionInfo.java +++ b/media/java/android/media/RoutingSessionInfo.java @@ -16,18 +16,25 @@ package android.media; +import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.res.Resources; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.os.UserHandle; import android.text.TextUtils; import com.android.internal.util.Preconditions; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -55,6 +62,33 @@ public final class RoutingSessionInfo implements Parcelable { private static final String KEY_GROUP_ROUTE = "androidx.mediarouter.media.KEY_GROUP_ROUTE"; private static final String KEY_VOLUME_HANDLING = "volumeHandling"; + /** + * Indicates that the transfer happened by the default logic without explicit system's or user's + * request. + * + * <p>For example, an automatically connected Bluetooth device will have this transfer reason. + */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public static final int TRANSFER_REASON_FALLBACK = 0; + + /** Indicates that the transfer happened from within a privileged application. */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public static final int TRANSFER_REASON_SYSTEM_REQUEST = 1; + + /** Indicates that the transfer happened from a non-privileged app. */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public static final int TRANSFER_REASON_APP = 2; + + /** + * Indicates the transfer reason. + * + * @hide + */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + @IntDef(value = {TRANSFER_REASON_FALLBACK, TRANSFER_REASON_SYSTEM_REQUEST, TRANSFER_REASON_APP}) + @Retention(RetentionPolicy.SOURCE) + public @interface TransferReason {} + @NonNull final String mId; @Nullable @@ -82,6 +116,10 @@ public final class RoutingSessionInfo implements Parcelable { final Bundle mControlHints; final boolean mIsSystemSession; + @TransferReason final int mTransferReason; + + @Nullable final UserHandle mTransferInitiatorUserHandle; + @Nullable final String mTransferInitiatorPackageName; RoutingSessionInfo(@NonNull Builder builder) { Objects.requireNonNull(builder, "builder must not be null."); @@ -116,6 +154,9 @@ public final class RoutingSessionInfo implements Parcelable { volumeAdjustmentForRemoteGroupSessions); mControlHints = updateVolumeHandlingInHints(builder.mControlHints, mVolumeHandling); + mTransferReason = builder.mTransferReason; + mTransferInitiatorUserHandle = builder.mTransferInitiatorUserHandle; + mTransferInitiatorPackageName = builder.mTransferInitiatorPackageName; } RoutingSessionInfo(@NonNull Parcel src) { @@ -140,6 +181,9 @@ public final class RoutingSessionInfo implements Parcelable { mControlHints = src.readBundle(); mIsSystemSession = src.readBoolean(); + mTransferReason = src.readInt(); + mTransferInitiatorUserHandle = src.readParcelable(null, android.os.UserHandle.class); + mTransferInitiatorPackageName = src.readString(); } @Nullable @@ -330,6 +374,27 @@ public final class RoutingSessionInfo implements Parcelable { return mIsSystemSession; } + /** Returns the transfer reason for this routing session. */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + @TransferReason + public int getTransferReason() { + return mTransferReason; + } + + /** @hide */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + @Nullable + public UserHandle getTransferInitiatorUserHandle() { + return mTransferInitiatorUserHandle; + } + + /** @hide */ + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + @Nullable + public String getTransferInitiatorPackageName() { + return mTransferInitiatorPackageName; + } + @Override public int describeContents() { return 0; @@ -351,6 +416,13 @@ public final class RoutingSessionInfo implements Parcelable { dest.writeInt(mVolume); dest.writeBundle(mControlHints); dest.writeBoolean(mIsSystemSession); + dest.writeInt(mTransferReason); + if (mTransferInitiatorUserHandle != null) { + mTransferInitiatorUserHandle.writeToParcel(dest, /* flags= */ 0); + } else { + dest.writeParcelable(null, /* flags= */ 0); + } + dest.writeString(mTransferInitiatorPackageName); } /** @@ -379,6 +451,9 @@ public final class RoutingSessionInfo implements Parcelable { pw.println(indent + "mVolume=" + mVolume); pw.println(indent + "mControlHints=" + mControlHints); pw.println(indent + "mIsSystemSession=" + mIsSystemSession); + pw.println(indent + "mTransferReason=" + mTransferReason); + pw.println(indent + "mtransferInitiatorUserHandle=" + mTransferInitiatorUserHandle); + pw.println(indent + "mtransferInitiatorPackageName=" + mTransferInitiatorPackageName); } @Override @@ -406,39 +481,69 @@ public final class RoutingSessionInfo implements Parcelable { && Objects.equals(mTransferableRoutes, other.mTransferableRoutes) && (mVolumeHandling == other.mVolumeHandling) && (mVolumeMax == other.mVolumeMax) - && (mVolume == other.mVolume); + && (mVolume == other.mVolume) + && (mTransferReason == other.mTransferReason) + && Objects.equals(mTransferInitiatorUserHandle, other.mTransferInitiatorUserHandle) + && Objects.equals( + mTransferInitiatorPackageName, other.mTransferInitiatorPackageName); } @Override public int hashCode() { - return Objects.hash(mId, mName, mOwnerPackageName, mClientPackageName, mProviderId, - mSelectedRoutes, mSelectableRoutes, mDeselectableRoutes, mTransferableRoutes, - mVolumeMax, mVolumeHandling, mVolume); + return Objects.hash( + mId, + mName, + mOwnerPackageName, + mClientPackageName, + mProviderId, + mSelectedRoutes, + mSelectableRoutes, + mDeselectableRoutes, + mTransferableRoutes, + mVolumeMax, + mVolumeHandling, + mVolume, + mTransferReason, + mTransferInitiatorUserHandle, + mTransferInitiatorPackageName); } @Override public String toString() { - StringBuilder result = new StringBuilder() - .append("RoutingSessionInfo{ ") - .append("sessionId=").append(getId()) - .append(", name=").append(getName()) - .append(", clientPackageName=").append(getClientPackageName()) - .append(", selectedRoutes={") - .append(String.join(",", getSelectedRoutes())) - .append("}") - .append(", selectableRoutes={") - .append(String.join(",", getSelectableRoutes())) - .append("}") - .append(", deselectableRoutes={") - .append(String.join(",", getDeselectableRoutes())) - .append("}") - .append(", transferableRoutes={") - .append(String.join(",", getTransferableRoutes())) - .append("}") - .append(", volumeHandling=").append(getVolumeHandling()) - .append(", volumeMax=").append(getVolumeMax()) - .append(", volume=").append(getVolume()) - .append(" }"); + StringBuilder result = + new StringBuilder() + .append("RoutingSessionInfo{ ") + .append("sessionId=") + .append(getId()) + .append(", name=") + .append(getName()) + .append(", clientPackageName=") + .append(getClientPackageName()) + .append(", selectedRoutes={") + .append(String.join(",", getSelectedRoutes())) + .append("}") + .append(", selectableRoutes={") + .append(String.join(",", getSelectableRoutes())) + .append("}") + .append(", deselectableRoutes={") + .append(String.join(",", getDeselectableRoutes())) + .append("}") + .append(", transferableRoutes={") + .append(String.join(",", getTransferableRoutes())) + .append("}") + .append(", volumeHandling=") + .append(getVolumeHandling()) + .append(", volumeMax=") + .append(getVolumeMax()) + .append(", volume=") + .append(getVolume()) + .append(", transferReason=") + .append(getTransferReason()) + .append(", transferInitiatorUserHandle=") + .append(getTransferInitiatorUserHandle()) + .append(", transferInitiatorPackageName=") + .append(getTransferInitiatorPackageName()) + .append(" }"); return result.toString(); } @@ -494,6 +599,9 @@ public final class RoutingSessionInfo implements Parcelable { @Nullable private Bundle mControlHints; private boolean mIsSystemSession; + @TransferReason private int mTransferReason = TRANSFER_REASON_FALLBACK; + @Nullable private UserHandle mTransferInitiatorUserHandle; + @Nullable private String mTransferInitiatorPackageName; /** * Constructor for builder to create {@link RoutingSessionInfo}. @@ -555,6 +663,9 @@ public final class RoutingSessionInfo implements Parcelable { mControlHints = sessionInfo.mControlHints; mIsSystemSession = sessionInfo.mIsSystemSession; + mTransferReason = sessionInfo.mTransferReason; + mTransferInitiatorUserHandle = sessionInfo.mTransferInitiatorUserHandle; + mTransferInitiatorPackageName = sessionInfo.mTransferInitiatorPackageName; } /** @@ -784,6 +895,35 @@ public final class RoutingSessionInfo implements Parcelable { } /** + * Sets transfer reason for the current session. + * + * <p>By default the transfer reason is set to {@link + * RoutingSessionInfo#TRANSFER_REASON_FALLBACK}. + */ + @NonNull + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public Builder setTransferReason(@TransferReason int transferReason) { + mTransferReason = transferReason; + return this; + } + + /** + * Sets the user handle and package name of the process that initiated the transfer. + * + * <p>By default the transfer initiation user handle and package name are set to {@code + * null}. + */ + @NonNull + @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) + public Builder setTransferInitiator( + @Nullable UserHandle transferInitiatorUserHandle, + @Nullable String transferInitiatorPackageName) { + mTransferInitiatorUserHandle = transferInitiatorUserHandle; + mTransferInitiatorPackageName = transferInitiatorPackageName; + return this; + } + + /** * Builds a routing session info. * * @throws IllegalArgumentException if no selected routes are added. diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 07f63e5441af..3da52ccbafed 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -69,3 +69,11 @@ flag { description: "Use BluetoothDevice.getAlias to populate the name of Bluetooth MediaRoute2Infos." bug: "314324170" } + +flag { + name: "enable_built_in_speaker_route_suitability_statuses" + namespace: "media_solutions" + description: "Make MediaRoute2Info provide information about routes suitability for transfer." + bug: "279555229" +} + diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java index 8ed4bf2b9cc3..c836df3b2c4d 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java @@ -385,7 +385,9 @@ public class MediaRouter2ManagerTest { MediaRoute2Info routeToSelect = routes.get(ROUTE_ID1); assertThat(routeToSelect).isNotNull(); - mManager.transfer(mPackageName, routeToSelect); + mManager.transfer( + mPackageName, routeToSelect, + android.os.Process.myUserHandle()); assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); assertThat(mManager.getRemoteSessions()).hasSize(1); } @@ -411,7 +413,9 @@ public class MediaRouter2ManagerTest { assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(1); - mManager.transfer(mPackageName, routeToSelect); + mManager.transfer( + mPackageName, routeToSelect, + android.os.Process.myUserHandle()); assertThat(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName); @@ -450,7 +454,11 @@ public class MediaRouter2ManagerTest { .addFeature(FEATURE_REMOTE_PLAYBACK) .build(); - mManager.transfer(mManager.getSystemRoutingSession(null), unknownRoute); + mManager.transfer( + mManager.getSystemRoutingSession(null), + unknownRoute, + android.os.Process.myUserHandle(), + mContext.getPackageName()); assertThat(onSessionCreatedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)).isFalse(); assertThat(onTransferFailedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); } @@ -484,7 +492,11 @@ public class MediaRouter2ManagerTest { assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(1); assertThat(mRouter2.getControllers()).hasSize(1); - mManager.transfer(mManager.getRoutingSessions(mPackageName).get(0), routeToSelect); + mManager.transfer( + mManager.getRoutingSessions(mPackageName).get(0), + routeToSelect, + android.os.Process.myUserHandle(), + mContext.getPackageName()); assertThat(transferLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); assertThat(mManager.getRoutingSessions(mPackageName)).hasSize(2); @@ -516,7 +528,11 @@ public class MediaRouter2ManagerTest { } }); awaitOnRouteChangedManager( - () -> mManager.transfer(mPackageName, routes.get(ROUTE_ID1)), + () -> + mManager.transfer( + mPackageName, + routes.get(ROUTE_ID1), + android.os.Process.myUserHandle()), ROUTE_ID1, route -> TextUtils.equals(route.getClientPackageName(), mPackageName)); assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); @@ -527,7 +543,11 @@ public class MediaRouter2ManagerTest { RoutingSessionInfo sessionInfo = sessions.get(1); awaitOnRouteChangedManager( - () -> mManager.transfer(mPackageName, routes.get(ROUTE_ID5_TO_TRANSFER_TO)), + () -> + mManager.transfer( + mPackageName, + routes.get(ROUTE_ID5_TO_TRANSFER_TO), + android.os.Process.myUserHandle()), ROUTE_ID5_TO_TRANSFER_TO, route -> TextUtils.equals(route.getClientPackageName(), mPackageName)); @@ -585,9 +605,11 @@ public class MediaRouter2ManagerTest { assertThat(route1).isNotNull(); assertThat(route2).isNotNull(); - mManager.transfer(mPackageName, route1); + mManager.transfer( + mPackageName, route1, android.os.Process.myUserHandle()); assertThat(successLatch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); - mManager.transfer(mPackageName, route2); + mManager.transfer( + mPackageName, route2, android.os.Process.myUserHandle()); assertThat(successLatch2.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); // onTransferFailed/onSessionReleased should not be called. @@ -634,7 +656,11 @@ public class MediaRouter2ManagerTest { List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName); RoutingSessionInfo targetSession = sessions.get(sessions.size() - 1); - mManager.transfer(targetSession, routes.get(ROUTE_ID6_TO_BE_IGNORED)); + mManager.transfer( + targetSession, + routes.get(ROUTE_ID6_TO_BE_IGNORED), + android.os.Process.myUserHandle(), + mContext.getPackageName()); assertThat(onSessionCreatedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)).isFalse(); assertThat(onFailedLatch.await(MediaRouter2Manager.TRANSFER_TIMEOUT_MS, @@ -705,7 +731,10 @@ public class MediaRouter2ManagerTest { } }); - mManager.transfer(mPackageName, routes.get(ROUTE_ID1)); + mManager.transfer( + mPackageName, + routes.get(ROUTE_ID1), + android.os.Process.myUserHandle()); assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); List<RoutingSessionInfo> sessions = mManager.getRoutingSessions(mPackageName); @@ -860,7 +889,8 @@ public class MediaRouter2ManagerTest { }); mRouter2.setOnGetControllerHintsListener(listener); - mManager.transfer(mPackageName, route); + mManager.transfer( + mPackageName, route, android.os.Process.myUserHandle()); assertThat(hintLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); assertThat(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); @@ -905,7 +935,10 @@ public class MediaRouter2ManagerTest { } }); - mManager.transfer(mPackageName, routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT)); + mManager.transfer( + mPackageName, + routes.get(ROUTE_ID4_TO_SELECT_AND_DESELECT), + android.os.Process.myUserHandle()); assertThat(onSessionCreatedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java index d5b5af7a98c4..97bbf12fd055 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java @@ -81,14 +81,17 @@ public class ManagerInfoMediaManager extends InfoMediaManager { @Override protected void transferToRoute(@NonNull MediaRoute2Info route) { - mRouterManager.transfer(mPackageName, route); + // TODO: b/279555229 - provide real user handle of a caller. + mRouterManager.transfer(mPackageName, route, android.os.Process.myUserHandle()); } @Override protected boolean connectDeviceWithoutPackageName(@NonNull MediaDevice device) { final RoutingSessionInfo info = mRouterManager.getSystemRoutingSession(null); if (info != null) { - mRouterManager.transfer(info, device.mRouteInfo); + // TODO: b/279555229 - provide real user handle and package name of a caller. + mRouterManager.transfer( + info, device.mRouteInfo, android.os.Process.myUserHandle(), mPackageName); return true; } return false; diff --git a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java index 850449595d74..0eb9166371dc 100644 --- a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java +++ b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java @@ -87,6 +87,8 @@ import java.util.Objects; @NonNull private final AudioDeviceCallback mAudioDeviceCallback = new AudioDeviceCallbackImpl(); + @MediaRoute2Info.SuitabilityStatus private final int mBuiltInSpeakerSuitabilityStatus; + @NonNull private final AudioManager.OnDevicesForAttributesChangedListener mOnDevicesForAttributesChangedListener = this::onDevicesForAttributesChangedListener; @@ -113,6 +115,10 @@ import java.util.Objects; mHandler = new Handler(Objects.requireNonNull(looper)); mStrategyForMedia = Objects.requireNonNull(strategyForMedia); mOnDeviceRouteChangedListener = Objects.requireNonNull(onDeviceRouteChangedListener); + + mBuiltInSpeakerSuitabilityStatus = + DeviceRouteController.getBuiltInSpeakerSuitabilityStatus(mContext); + mBluetoothRouteController = new AudioPoliciesBluetoothRouteController( mContext, btAdapter, this::rebuildAvailableRoutesAndNotify); @@ -373,14 +379,19 @@ import java.util.Objects; // from getting an id using BluetoothRouteController#getRouteIdForBluetoothAddress. routeId = systemRouteInfo.mDefaultRouteId; } - return new MediaRoute2Info.Builder(routeId, humanReadableName) + MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(routeId, humanReadableName) .setType(systemRouteInfo.mMediaRoute2InfoType) .setAddress(address) .setSystemRoute(true) .addFeature(FEATURE_LIVE_AUDIO) .addFeature(FEATURE_LOCAL_PLAYBACK) - .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED) - .build(); + .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED); + + if (systemRouteInfo.mMediaRoute2InfoType == MediaRoute2Info.TYPE_BUILTIN_SPEAKER) { + builder.setSuitabilityStatus(mBuiltInSpeakerSuitabilityStatus); + } + + return builder.build(); } /** diff --git a/services/core/java/com/android/server/media/DeviceRouteController.java b/services/core/java/com/android/server/media/DeviceRouteController.java index 9f175a9a0277..8b62cc974862 100644 --- a/services/core/java/com/android/server/media/DeviceRouteController.java +++ b/services/core/java/com/android/server/media/DeviceRouteController.java @@ -81,6 +81,30 @@ import java.util.List; } } + /** Returns device route availability status. */ + @MediaRoute2Info.SuitabilityStatus + static int getBuiltInSpeakerSuitabilityStatus(@NonNull Context context) { + if (!Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { + // Route is always suitable if the flag is disabled. + return MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER; + } + + int availabilityStatus = + context.getResources() + .getInteger( + com.android.internal.R.integer + .config_mediaRouter_builtInSpeakerSuitability); + + switch (availabilityStatus) { + case MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER: + case MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_MANUAL_TRANSFER: + case MediaRoute2Info.SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER: + return availabilityStatus; + default: + return MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER; + } + } + /** Returns the currently selected device (built-in or wired) route. */ @NonNull MediaRoute2Info getSelectedRoute(); diff --git a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java index c0f28346705c..65b0ad0d61a0 100644 --- a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java +++ b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java @@ -72,6 +72,8 @@ import java.util.Objects; @NonNull private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver(); + @MediaRoute2Info.SuitabilityStatus private final int mBuiltInSpeakerSuitabilityStatus; + private int mDeviceVolume; private MediaRoute2Info mDeviceRoute; @@ -90,6 +92,9 @@ import java.util.Objects; mAudioManager = audioManager; mAudioService = audioService; + mBuiltInSpeakerSuitabilityStatus = + DeviceRouteController.getBuiltInSpeakerSuitabilityStatus(mContext); + AudioRoutesInfo newAudioRoutes = null; try { newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver); @@ -165,19 +170,28 @@ import java.util.Objects; } synchronized (this) { - return new MediaRoute2Info.Builder( - DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString()) - .setVolumeHandling(mAudioManager.isVolumeFixed() - ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED - : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) - .setVolume(mDeviceVolume) - .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) - .setType(type) - .addFeature(FEATURE_LIVE_AUDIO) - .addFeature(FEATURE_LIVE_VIDEO) - .addFeature(FEATURE_LOCAL_PLAYBACK) - .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED) - .build(); + MediaRoute2Info.Builder builder = + new MediaRoute2Info.Builder( + DEVICE_ROUTE_ID, + mContext.getResources().getText(name).toString()) + .setVolumeHandling( + mAudioManager.isVolumeFixed() + ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED + : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) + .setVolume(mDeviceVolume) + .setVolumeMax( + mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) + .setType(type) + .addFeature(FEATURE_LIVE_AUDIO) + .addFeature(FEATURE_LIVE_VIDEO) + .addFeature(FEATURE_LOCAL_PLAYBACK) + .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED); + + if (type == TYPE_BUILTIN_SPEAKER) { + builder.setSuitabilityStatus(mBuiltInSpeakerSuitabilityStatus); + } + + return builder.build(); } } diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java index 8149847d70c0..1bc2a5eb1351 100644 --- a/services/core/java/com/android/server/media/MediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java @@ -24,6 +24,7 @@ import android.media.MediaRoute2ProviderInfo; import android.media.RouteDiscoveryPreference; import android.media.RoutingSessionInfo; import android.os.Bundle; +import android.os.UserHandle; import com.android.internal.annotations.GuardedBy; @@ -54,8 +55,15 @@ abstract class MediaRoute2Provider { mCallback = callback; } - public abstract void requestCreateSession(long requestId, String packageName, String routeId, - @Nullable Bundle sessionHints); + public abstract void requestCreateSession( + long requestId, + String packageName, + String routeId, + @Nullable Bundle sessionHints, + @RoutingSessionInfo.TransferReason int transferReason, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName); + public abstract void releaseSession(long requestId, String sessionId); public abstract void updateDiscoveryPreference( @@ -63,7 +71,14 @@ abstract class MediaRoute2Provider { public abstract void selectRoute(long requestId, String sessionId, String routeId); public abstract void deselectRoute(long requestId, String sessionId, String routeId); - public abstract void transferToRoute(long requestId, String sessionId, String routeId); + + public abstract void transferToRoute( + long requestId, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName, + String sessionId, + String routeId, + @RoutingSessionInfo.TransferReason int transferReason); public abstract void setRouteVolume(long requestId, String routeId, int volume); public abstract void setSessionVolume(long requestId, String sessionId, int volume); diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index 330818ed17ca..ae889d8255c6 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -98,8 +98,14 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider } @Override - public void requestCreateSession(long requestId, String packageName, String routeId, - Bundle sessionHints) { + public void requestCreateSession( + long requestId, + String packageName, + String routeId, + Bundle sessionHints, + @RoutingSessionInfo.TransferReason int transferReason, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { if (mConnectionReady) { mActiveConnection.requestCreateSession(requestId, packageName, routeId, sessionHints); updateBinding(); @@ -141,7 +147,13 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider } @Override - public void transferToRoute(long requestId, String sessionId, String routeId) { + public void transferToRoute( + long requestId, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName, + String sessionId, + String routeId, + @RoutingSessionInfo.TransferReason int transferReason) { if (mConnectionReady) { mActiveConnection.transferToRoute(requestId, sessionId, routeId); } @@ -649,6 +661,14 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider + "Disallowed route: " + route); } + + if (route.getSuitabilityStatus() + == MediaRoute2Info.SUITABILITY_STATUS_NOT_SUITABLE_FOR_TRANSFER) { + throw new SecurityException( + "Only the system is allowed to set not suitable for transfer status. " + + "Disallowed route: " + + route); + } } Connection connection = mConnectionRef.get(); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 5e18727459c6..38f0df41db04 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -337,18 +337,47 @@ class MediaRouter2ServiceImpl { } } - public void requestCreateSessionWithRouter2(@NonNull IMediaRouter2 router, int requestId, - long managerRequestId, @NonNull RoutingSessionInfo oldSession, - @NonNull MediaRoute2Info route, Bundle sessionHints) { + public void requestCreateSessionWithRouter2( + @NonNull IMediaRouter2 router, + int requestId, + long managerRequestId, + @NonNull RoutingSessionInfo oldSession, + @NonNull MediaRoute2Info route, + Bundle sessionHints, + @Nullable UserHandle transferInitiatorUserHandle, + @Nullable String transferInitiatorPackageName) { Objects.requireNonNull(router, "router must not be null"); Objects.requireNonNull(oldSession, "oldSession must not be null"); Objects.requireNonNull(route, "route must not be null"); + synchronized (mLock) { + if (managerRequestId == MediaRoute2ProviderService.REQUEST_ID_NONE + || transferInitiatorUserHandle == null + || transferInitiatorPackageName == null) { + final IBinder binder = router.asBinder(); + final RouterRecord routerRecord = mAllRouterRecords.get(binder); + + transferInitiatorUserHandle = Binder.getCallingUserHandle(); + if (routerRecord != null) { + transferInitiatorPackageName = routerRecord.mPackageName; + } else { + transferInitiatorPackageName = mContext.getPackageName(); + } + } + } + final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - requestCreateSessionWithRouter2Locked(requestId, managerRequestId, - router, oldSession, route, sessionHints); + requestCreateSessionWithRouter2Locked( + requestId, + managerRequestId, + transferInitiatorUserHandle, + transferInitiatorPackageName, + router, + oldSession, + route, + sessionHints); } } finally { Binder.restoreCallingIdentity(token); @@ -399,10 +428,11 @@ class MediaRouter2ServiceImpl { throw new IllegalArgumentException("uniqueSessionId must not be empty"); } + UserHandle userHandle = Binder.getCallingUserHandle(); final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - transferToRouteWithRouter2Locked(router, uniqueSessionId, route); + transferToRouteWithRouter2Locked(router, userHandle, uniqueSessionId, route); } } finally { Binder.restoreCallingIdentity(token); @@ -588,16 +618,28 @@ class MediaRouter2ServiceImpl { } } - public void requestCreateSessionWithManager(@NonNull IMediaRouter2Manager manager, - int requestId, @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) { + public void requestCreateSessionWithManager( + @NonNull IMediaRouter2Manager manager, + int requestId, + @NonNull RoutingSessionInfo oldSession, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { Objects.requireNonNull(manager, "manager must not be null"); Objects.requireNonNull(oldSession, "oldSession must not be null"); Objects.requireNonNull(route, "route must not be null"); + Objects.requireNonNull(transferInitiatorUserHandle); final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - requestCreateSessionWithManagerLocked(requestId, manager, oldSession, route); + requestCreateSessionWithManagerLocked( + requestId, + manager, + oldSession, + route, + transferInitiatorUserHandle, + transferInitiatorPackageName); } } finally { Binder.restoreCallingIdentity(token); @@ -640,18 +682,32 @@ class MediaRouter2ServiceImpl { } } - public void transferToRouteWithManager(@NonNull IMediaRouter2Manager manager, int requestId, - @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { + public void transferToRouteWithManager( + @NonNull IMediaRouter2Manager manager, + int requestId, + @NonNull String uniqueSessionId, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { Objects.requireNonNull(manager, "manager must not be null"); if (TextUtils.isEmpty(uniqueSessionId)) { throw new IllegalArgumentException("uniqueSessionId must not be empty"); } Objects.requireNonNull(route, "route must not be null"); + Objects.requireNonNull(transferInitiatorUserHandle); + Objects.requireNonNull(transferInitiatorPackageName); final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - transferToRouteWithManagerLocked(requestId, manager, uniqueSessionId, route); + transferToRouteWithManagerLocked( + requestId, + manager, + uniqueSessionId, + route, + RoutingSessionInfo.TRANSFER_REASON_SYSTEM_REQUEST, + transferInitiatorUserHandle, + transferInitiatorPackageName); } } finally { Binder.restoreCallingIdentity(token); @@ -1038,9 +1094,15 @@ class MediaRouter2ServiceImpl { } @GuardedBy("mLock") - private void requestCreateSessionWithRouter2Locked(int requestId, long managerRequestId, - @NonNull IMediaRouter2 router, @NonNull RoutingSessionInfo oldSession, - @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) { + private void requestCreateSessionWithRouter2Locked( + int requestId, + long managerRequestId, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName, + @NonNull IMediaRouter2 router, + @NonNull RoutingSessionInfo oldSession, + @NonNull MediaRoute2Info route, + @Nullable Bundle sessionHints) { final IBinder binder = router.asBinder(); final RouterRecord routerRecord = mAllRouterRecords.get(binder); @@ -1114,9 +1176,16 @@ class MediaRouter2ServiceImpl { long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId); routerRecord.mUserRecord.mHandler.sendMessage( - obtainMessage(UserHandler::requestCreateSessionWithRouter2OnHandler, + obtainMessage( + UserHandler::requestCreateSessionWithRouter2OnHandler, routerRecord.mUserRecord.mHandler, - uniqueRequestId, managerRequestId, routerRecord, oldSession, route, + uniqueRequestId, + managerRequestId, + transferInitiatorUserHandle, + transferInitiatorPackageName, + routerRecord, + oldSession, + route, sessionHints)); } @@ -1165,8 +1234,11 @@ class MediaRouter2ServiceImpl { } @GuardedBy("mLock") - private void transferToRouteWithRouter2Locked(@NonNull IMediaRouter2 router, - @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { + private void transferToRouteWithRouter2Locked( + @NonNull IMediaRouter2 router, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String uniqueSessionId, + @NonNull MediaRoute2Info route) { final IBinder binder = router.asBinder(); final RouterRecord routerRecord = mAllRouterRecords.get(binder); @@ -1191,9 +1263,16 @@ class MediaRouter2ServiceImpl { routerRecord, toOriginalRequestId(DUMMY_REQUEST_ID))); } else { routerRecord.mUserRecord.mHandler.sendMessage( - obtainMessage(UserHandler::transferToRouteOnHandler, + obtainMessage( + UserHandler::transferToRouteOnHandler, routerRecord.mUserRecord.mHandler, - DUMMY_REQUEST_ID, routerRecord, uniqueSessionId, route)); + DUMMY_REQUEST_ID, + transferInitiatorUserHandle, + routerRecord.mPackageName, + routerRecord, + uniqueSessionId, + route, + RoutingSessionInfo.TRANSFER_REASON_APP)); } } @@ -1416,9 +1495,13 @@ class MediaRouter2ServiceImpl { } @GuardedBy("mLock") - private void requestCreateSessionWithManagerLocked(int requestId, - @NonNull IMediaRouter2Manager manager, @NonNull RoutingSessionInfo oldSession, - @NonNull MediaRoute2Info route) { + private void requestCreateSessionWithManagerLocked( + int requestId, + @NonNull IMediaRouter2Manager manager, + @NonNull RoutingSessionInfo oldSession, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder()); if (managerRecord == null) { return; @@ -1464,9 +1547,16 @@ class MediaRouter2ServiceImpl { // Before requesting to the provider, get session hints from the media router. // As a return, media router will request to create a session. routerRecord.mUserRecord.mHandler.sendMessage( - obtainMessage(UserHandler::requestRouterCreateSessionOnHandler, + obtainMessage( + UserHandler::requestRouterCreateSessionOnHandler, routerRecord.mUserRecord.mHandler, - uniqueRequestId, routerRecord, managerRecord, oldSession, route)); + uniqueRequestId, + routerRecord, + managerRecord, + oldSession, + route, + transferInitiatorUserHandle, + transferInitiatorPackageName)); } @GuardedBy("mLock") @@ -1521,9 +1611,14 @@ class MediaRouter2ServiceImpl { } @GuardedBy("mLock") - private void transferToRouteWithManagerLocked(int requestId, + private void transferToRouteWithManagerLocked( + int requestId, @NonNull IMediaRouter2Manager manager, - @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { + @NonNull String uniqueSessionId, + @NonNull MediaRoute2Info route, + @RoutingSessionInfo.TransferReason int transferReason, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { final IBinder binder = manager.asBinder(); ManagerRecord managerRecord = mAllManagerRecords.get(binder); @@ -1541,9 +1636,16 @@ class MediaRouter2ServiceImpl { long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); managerRecord.mUserRecord.mHandler.sendMessage( - obtainMessage(UserHandler::transferToRouteOnHandler, + obtainMessage( + UserHandler::transferToRouteOnHandler, managerRecord.mUserRecord.mHandler, - uniqueRequestId, routerRecord, uniqueSessionId, route)); + uniqueRequestId, + transferInitiatorUserHandle, + transferInitiatorPackageName, + routerRecord, + uniqueSessionId, + route, + transferReason)); } @GuardedBy("mLock") @@ -1850,6 +1952,19 @@ class MediaRouter2ServiceImpl { } } + public void notifySessionCreated(int requestId, @NonNull RoutingSessionInfo sessionInfo) { + try { + mRouter.notifySessionCreated( + requestId, maybeClearTransferInitiatorIdentity(sessionInfo)); + } catch (RemoteException ex) { + Slog.w( + TAG, + "Failed to notify router of the session creation." + + " Router probably died.", + ex); + } + } + /** * Sends the corresponding router an update for the given session. * @@ -1857,12 +1972,27 @@ class MediaRouter2ServiceImpl { */ public void notifySessionInfoChanged(RoutingSessionInfo sessionInfo) { try { - mRouter.notifySessionInfoChanged(sessionInfo); + mRouter.notifySessionInfoChanged(maybeClearTransferInitiatorIdentity(sessionInfo)); } catch (RemoteException ex) { Slog.w(TAG, "Failed to notify session info changed. Router probably died.", ex); } } + private RoutingSessionInfo maybeClearTransferInitiatorIdentity( + @NonNull RoutingSessionInfo sessionInfo) { + UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle(); + String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName(); + + if (!Objects.equals(UserHandle.of(mUserRecord.mUserId), transferInitiatorUserHandle) + || !Objects.equals(mPackageName, transferInitiatorPackageName)) { + return new RoutingSessionInfo.Builder(sessionInfo) + .setTransferInitiator(null, null) + .build(); + } + + return sessionInfo; + } + /** * Returns a filtered copy of {@code routes} that contains only the routes that are {@link * MediaRoute2Info#isVisibleTo visible} to the router corresponding to this record. @@ -2307,9 +2437,14 @@ class MediaRouter2ServiceImpl { return -1; } - private void requestRouterCreateSessionOnHandler(long uniqueRequestId, - @NonNull RouterRecord routerRecord, @NonNull ManagerRecord managerRecord, - @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) { + private void requestRouterCreateSessionOnHandler( + long uniqueRequestId, + @NonNull RouterRecord routerRecord, + @NonNull ManagerRecord managerRecord, + @NonNull RoutingSessionInfo oldSession, + @NonNull MediaRoute2Info route, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { try { if (route.isSystemRoute() && !routerRecord.hasSystemRoutingPermission()) { // The router lacks permission to modify system routing, so we hide system @@ -2317,7 +2452,11 @@ class MediaRouter2ServiceImpl { route = mSystemProvider.getDefaultRoute(); } routerRecord.mRouter.requestCreateSessionByManager( - uniqueRequestId, oldSession, route); + uniqueRequestId, + oldSession, + route, + transferInitiatorUserHandle, + transferInitiatorPackageName); } catch (RemoteException ex) { Slog.w(TAG, "getSessionHintsForCreatingSessionOnHandler: " + "Failed to request. Router probably died.", ex); @@ -2326,10 +2465,15 @@ class MediaRouter2ServiceImpl { } } - private void requestCreateSessionWithRouter2OnHandler(long uniqueRequestId, - long managerRequestId, @NonNull RouterRecord routerRecord, + private void requestCreateSessionWithRouter2OnHandler( + long uniqueRequestId, + long managerRequestId, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName, + @NonNull RouterRecord routerRecord, @NonNull RoutingSessionInfo oldSession, - @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) { + @NonNull MediaRoute2Info route, + @Nullable Bundle sessionHints) { final MediaRoute2Provider provider = findProvider(route.getProviderId()); if (provider == null) { @@ -2345,8 +2489,19 @@ class MediaRouter2ServiceImpl { managerRequestId, oldSession, route); mSessionCreationRequests.add(request); - provider.requestCreateSession(uniqueRequestId, routerRecord.mPackageName, - route.getOriginalId(), sessionHints); + int transferReason = RoutingSessionInfo.TRANSFER_REASON_APP; + if (managerRequestId != MediaRoute2ProviderService.REQUEST_ID_NONE) { + transferReason = RoutingSessionInfo.TRANSFER_REASON_SYSTEM_REQUEST; + } + + provider.requestCreateSession( + uniqueRequestId, + routerRecord.mPackageName, + route.getOriginalId(), + sessionHints, + transferReason, + transferInitiatorUserHandle, + transferInitiatorPackageName); } // routerRecord can be null if the session is system's or RCN. @@ -2386,9 +2541,14 @@ class MediaRouter2ServiceImpl { } // routerRecord can be null if the session is system's or RCN. - private void transferToRouteOnHandler(long uniqueRequestId, + private void transferToRouteOnHandler( + long uniqueRequestId, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName, @Nullable RouterRecord routerRecord, - @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { + @NonNull String uniqueSessionId, + @NonNull MediaRoute2Info route, + @RoutingSessionInfo.TransferReason int transferReason) { if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route, "transferring to")) { return; @@ -2399,8 +2559,13 @@ class MediaRouter2ServiceImpl { if (provider == null) { return; } - provider.transferToRoute(uniqueRequestId, getOriginalId(uniqueSessionId), - route.getOriginalId()); + provider.transferToRoute( + uniqueRequestId, + transferInitiatorUserHandle, + transferInitiatorPackageName, + getOriginalId(uniqueSessionId), + route.getOriginalId(), + transferReason); } // routerRecord is null if and only if the session is created without the request, which @@ -2535,6 +2700,7 @@ class MediaRouter2ServiceImpl { // session info from them. sessionInfo = mSystemProvider.getDefaultSessionInfo(); } + // TODO: b/279555229 - replace with matchingRequest.mRouterRecord.notifySessionCreated. notifySessionCreatedToRouter( matchingRequest.mRouterRecord, toOriginalRequestId(uniqueRequestId), @@ -2648,12 +2814,7 @@ class MediaRouter2ServiceImpl { private void notifySessionCreatedToRouter(@NonNull RouterRecord routerRecord, int requestId, @NonNull RoutingSessionInfo sessionInfo) { - try { - routerRecord.mRouter.notifySessionCreated(requestId, sessionInfo); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify router of the session creation." - + " Router probably died.", ex); - } + routerRecord.notifySessionCreated(requestId, sessionInfo); } private void notifySessionCreationFailedToRouter(@NonNull RouterRecord routerRecord, diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index e562b3f0845c..7dd13142f52e 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -461,11 +461,24 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override - public void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId, - long managerRequestId, RoutingSessionInfo oldSession, - MediaRoute2Info route, Bundle sessionHints) { - mService2.requestCreateSessionWithRouter2(router, requestId, managerRequestId, - oldSession, route, sessionHints); + public void requestCreateSessionWithRouter2( + IMediaRouter2 router, + int requestId, + long managerRequestId, + RoutingSessionInfo oldSession, + MediaRoute2Info route, + Bundle sessionHints, + @Nullable UserHandle transferInitiatorUserHandle, + @Nullable String transferInitiatorPackageName) { + mService2.requestCreateSessionWithRouter2( + router, + requestId, + managerRequestId, + oldSession, + route, + sessionHints, + transferInitiatorUserHandle, + transferInitiatorPackageName); } // Binder call @@ -580,9 +593,20 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override - public void requestCreateSessionWithManager(IMediaRouter2Manager manager, - int requestId, RoutingSessionInfo oldSession, MediaRoute2Info route) { - mService2.requestCreateSessionWithManager(manager, requestId, oldSession, route); + public void requestCreateSessionWithManager( + IMediaRouter2Manager manager, + int requestId, + RoutingSessionInfo oldSession, + MediaRoute2Info route, + UserHandle transferInitiatorUserHandle, + String transferInitiatorPackageName) { + mService2.requestCreateSessionWithManager( + manager, + requestId, + oldSession, + route, + transferInitiatorUserHandle, + transferInitiatorPackageName); } // Binder call @@ -601,9 +625,20 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override - public void transferToRouteWithManager(IMediaRouter2Manager manager, int requestId, - String sessionId, MediaRoute2Info route) { - mService2.transferToRouteWithManager(manager, requestId, sessionId, route); + public void transferToRouteWithManager( + IMediaRouter2Manager manager, + int requestId, + String sessionId, + MediaRoute2Info route, + UserHandle transferInitiatorUserHandle, + String transferInitiatorPackageName) { + mService2.transferToRouteWithManager( + manager, + requestId, + sessionId, + route, + transferInitiatorUserHandle, + transferInitiatorPackageName); } // Binder call diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 9d151c27e7c7..f7210dd1ef70 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -16,6 +16,7 @@ package com.android.server.media; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -26,6 +27,7 @@ import android.media.AudioManager; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; import android.media.MediaRoute2ProviderService; +import android.media.MediaRouter2Utils; import android.media.RouteDiscoveryPreference; import android.media.RoutingSessionInfo; import android.os.Bundle; @@ -39,6 +41,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.media.flags.Flags; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; @@ -79,6 +82,10 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { @GuardedBy("mRequestLock") private volatile SessionCreationRequest mPendingSessionCreationRequest; + private final Object mTransferLock = new Object(); + @GuardedBy("mTransferLock") + @Nullable private volatile SessionCreationRequest mPendingTransferRequest; + SystemMediaRoute2Provider(Context context, UserHandle user) { super(COMPONENT_NAME); mIsSystemRouteProvider = true; @@ -146,17 +153,30 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } @Override - public void requestCreateSession(long requestId, String packageName, String routeId, - Bundle sessionHints) { + public void requestCreateSession( + long requestId, + String packageName, + String routeId, + Bundle sessionHints, + @RoutingSessionInfo.TransferReason int transferReason, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { // Assume a router without MODIFY_AUDIO_ROUTING permission can't request with // a route ID different from the default route ID. The service should've filtered. if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { mCallback.onSessionCreated(this, requestId, mDefaultSessionInfo); return; } - if (TextUtils.equals(routeId, mSelectedRouteId)) { - mCallback.onSessionCreated(this, requestId, mSessionInfos.get(0)); - return; + + if (!Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { + if (TextUtils.equals(routeId, mSelectedRouteId)) { + RoutingSessionInfo currentSessionInfo; + synchronized (mLock) { + currentSessionInfo = mSessionInfos.get(0); + } + mCallback.onSessionCreated(this, requestId, currentSessionInfo); + return; + } } synchronized (mRequestLock) { @@ -165,10 +185,23 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { mCallback.onRequestFailed(this, mPendingSessionCreationRequest.mRequestId, MediaRoute2ProviderService.REASON_UNKNOWN_ERROR); } - mPendingSessionCreationRequest = new SessionCreationRequest(requestId, routeId); + mPendingSessionCreationRequest = + new SessionCreationRequest( + requestId, + routeId, + RoutingSessionInfo.TRANSFER_REASON_FALLBACK, + transferInitiatorUserHandle, + transferInitiatorPackageName); } - transferToRoute(requestId, SYSTEM_SESSION_ID, routeId); + // Only unprivileged routers call this method, therefore we use TRANSFER_REASON_APP. + transferToRoute( + requestId, + transferInitiatorUserHandle, + transferInitiatorPackageName, + SYSTEM_SESSION_ID, + routeId, + transferReason); } @Override @@ -193,12 +226,31 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } @Override - public void transferToRoute(long requestId, String sessionId, String routeId) { + public void transferToRoute( + long requestId, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName, + String sessionId, + String routeId, + @RoutingSessionInfo.TransferReason int transferReason) { if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { // The currently selected route is the default route. Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT); return; } + + if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { + synchronized (mTransferLock) { + mPendingTransferRequest = + new SessionCreationRequest( + requestId, + routeId, + transferReason, + transferInitiatorUserHandle, + transferInitiatorPackageName); + } + } + MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute(); boolean isAvailableDeviceRoute = mDeviceRouteController.getAvailableRoutes().stream() @@ -218,6 +270,11 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { mDeviceRouteController.transferTo(null); mBluetoothRouteController.transferTo(routeId); } + + if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses() + && updateSessionInfosIfNeeded()) { + notifySessionInfoUpdated(); + } } @Override @@ -322,9 +379,11 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute(); MediaRoute2Info selectedRoute = selectedDeviceRoute; MediaRoute2Info selectedBtRoute = mBluetoothRouteController.getSelectedRoute(); + List<String> transferableRoutes = new ArrayList<>(); + if (selectedBtRoute != null) { selectedRoute = selectedBtRoute; - builder.addTransferableRoute(selectedDeviceRoute.getId()); + transferableRoutes.add(selectedDeviceRoute.getId()); } mSelectedRouteId = selectedRoute.getId(); mDefaultRoute = @@ -337,12 +396,54 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { for (MediaRoute2Info route : mDeviceRouteController.getAvailableRoutes()) { String routeId = route.getId(); if (!mSelectedRouteId.equals(routeId)) { - builder.addTransferableRoute(routeId); + transferableRoutes.add(routeId); } } } for (MediaRoute2Info route : mBluetoothRouteController.getTransferableRoutes()) { - builder.addTransferableRoute(route.getId()); + transferableRoutes.add(route.getId()); + } + + for (String route : transferableRoutes) { + builder.addTransferableRoute(route); + } + + if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { + int transferReason = RoutingSessionInfo.TRANSFER_REASON_FALLBACK; + UserHandle transferInitiatorUserHandle = null; + String transferInitiatorPackageName = null; + + if (oldSessionInfo != null + && containsSelectedRouteWithId(oldSessionInfo, selectedRoute.getId())) { + transferReason = oldSessionInfo.getTransferReason(); + transferInitiatorUserHandle = oldSessionInfo.getTransferInitiatorUserHandle(); + transferInitiatorPackageName = oldSessionInfo.getTransferInitiatorPackageName(); + } + + synchronized (mTransferLock) { + if (mPendingTransferRequest != null) { + boolean isTransferringToTheSelectedRoute = + mPendingTransferRequest.isTargetRoute(selectedRoute); + boolean canBePotentiallyTransferred = + mPendingTransferRequest.isInsideOfRoutesList(transferableRoutes); + + if (isTransferringToTheSelectedRoute) { + transferReason = mPendingTransferRequest.mTransferReason; + transferInitiatorUserHandle = + mPendingTransferRequest.mTransferInitiatorUserHandle; + transferInitiatorPackageName = + mPendingTransferRequest.mTransferInitiatorPackageName; + + mPendingTransferRequest = null; + } else if (!canBePotentiallyTransferred) { + mPendingTransferRequest = null; + } + } + } + + builder.setTransferReason(transferReason) + .setTransferInitiator( + transferInitiatorUserHandle, transferInitiatorPackageName); } RoutingSessionInfo newSessionInfo = builder.setProviderId(mUniqueId).build(); @@ -424,6 +525,22 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { return false; } + private boolean containsSelectedRouteWithId( + @Nullable RoutingSessionInfo sessionInfo, @NonNull String selectedRouteId) { + if (sessionInfo == null) { + return false; + } + + List<String> selectedRoutes = sessionInfo.getSelectedRoutes(); + + if (selectedRoutes.size() != 1) { + throw new IllegalStateException("Selected routes list should contain only 1 route id."); + } + + String oldSelectedRouteId = MediaRouter2Utils.getOriginalId(selectedRoutes.get(0)); + return oldSelectedRouteId != null && oldSelectedRouteId.equals(selectedRouteId); + } + void publishProviderState() { updateProviderState(); notifyProviderState(); @@ -452,12 +569,47 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } private static class SessionCreationRequest { - final long mRequestId; - final String mRouteId; + private final long mRequestId; + @NonNull private final String mRouteId; + + @RoutingSessionInfo.TransferReason private final int mTransferReason; + + @NonNull private final UserHandle mTransferInitiatorUserHandle; + @NonNull private final String mTransferInitiatorPackageName; + + SessionCreationRequest( + long requestId, + @NonNull String routeId, + @RoutingSessionInfo.TransferReason int transferReason, + @NonNull UserHandle transferInitiatorUserHandle, + @NonNull String transferInitiatorPackageName) { + mRequestId = requestId; + mRouteId = routeId; + mTransferReason = transferReason; + mTransferInitiatorUserHandle = transferInitiatorUserHandle; + mTransferInitiatorPackageName = transferInitiatorPackageName; + } + + private boolean isTargetRoute(@Nullable MediaRoute2Info route2Info) { + if (route2Info == null) { + return false; + } + + return isTargetRoute(route2Info.getId()); + } + + private boolean isTargetRoute(@Nullable String routeId) { + return mRouteId.equals(routeId); + } + + private boolean isInsideOfRoutesList(@NonNull List<String> routesList) { + for (String routeId : routesList) { + if (isTargetRoute(routeId)) { + return true; + } + } - SessionCreationRequest(long requestId, String routeId) { - this.mRequestId = requestId; - this.mRouteId = routeId; + return false; } } |