diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index 7e238e44043bb..0e9c162e4929f 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -57,6 +57,30 @@ public final class MediaRoute2Info implements Parcelable { } }; + /** + * The {@link #getOriginalId() original id} of the route that represents the built-in media + * route. + * + *

A route with this id will only be visible to apps with permission to do system routing, + * which means having {@link android.Manifest.permission#BLUETOOTH_CONNECT} and {@link + * android.Manifest.permission#BLUETOOTH_SCAN}, or {@link + * android.Manifest.permission#MODIFY_AUDIO_ROUTING}. + * + * @hide + */ + public static final String ROUTE_ID_DEVICE = "DEVICE_ROUTE"; + + /** + * The {@link #getOriginalId() original id} of the route that represents the default system + * media route. + * + *

A route with this id will be visible to apps with no permission over system routing. See + * {@link #ROUTE_ID_DEVICE} for details. + * + * @hide + */ + public static final String ROUTE_ID_DEFAULT = "DEFAULT_ROUTE"; + /** @hide */ @IntDef({CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTING, CONNECTION_STATE_CONNECTED}) diff --git a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java index 182aa6fcef028..93f6ff3de3c28 100644 --- a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java +++ b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java @@ -42,13 +42,10 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.Objects; - /* package */ final class AudioPoliciesDeviceRouteController implements DeviceRouteController { private static final String TAG = "APDeviceRoutesController"; - private static final String DEVICE_ROUTE_ID = "DEVICE_ROUTE"; - @NonNull private final Context mContext; @NonNull @@ -182,10 +179,12 @@ 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) + MediaRoute2Info.ROUTE_ID_DEVICE, + 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) diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index cac22a6b8b208..b79991e5d0132 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -28,6 +28,7 @@ import static com.android.internal.util.function.pooled.PooledLambda.obtainMessa import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.app.ActivityManager; import android.app.ActivityThread; import android.content.BroadcastReceiver; @@ -75,8 +76,10 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -97,6 +100,17 @@ class MediaRouter2ServiceImpl { private static final String KEY_SCANNING_PACKAGE_MINIMUM_IMPORTANCE = "scanning_package_minimum_importance"; + /** + * Contains the list of bluetooth permissions that are required to do system routing. + * + *

Alternatively, apps that hold {@link android.Manifest.permission#MODIFY_AUDIO_ROUTING} are + * also allowed to do system routing. + */ + private static final String[] BLUETOOTH_PERMISSIONS_FOR_SYSTEM_ROUTING = + new String[] { + Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN + }; + private static int sPackageImportanceForScanning = DeviceConfig.getInt( MEDIA_BETTER_TOGETHER_NAMESPACE, /* name */ KEY_SCANNING_PACKAGE_MINIMUM_IMPORTANCE, @@ -142,6 +156,7 @@ class MediaRouter2ServiceImpl { } }; + @RequiresPermission(Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) /* package */ MediaRouter2ServiceImpl(Context context) { mContext = context; mActivityManager = mContext.getSystemService(ActivityManager.class); @@ -155,12 +170,28 @@ class MediaRouter2ServiceImpl { screenOnOffIntentFilter.addAction(ACTION_SCREEN_OFF); mContext.registerReceiver(mScreenOnOffReceiver, screenOnOffIntentFilter); + mContext.getPackageManager().addOnPermissionsChangeListener(this::onPermissionsChanged); DeviceConfig.addOnPropertiesChangedListener(MEDIA_BETTER_TOGETHER_NAMESPACE, ActivityThread.currentApplication().getMainExecutor(), this::onDeviceConfigChange); } + /** + * Called when there's a change in the permissions of an app. + * + * @param uid The uid of the app whose permissions changed. + */ + private void onPermissionsChanged(int uid) { + synchronized (mLock) { + Optional affectedRouter = + mAllRouterRecords.values().stream().filter(it -> it.mUid == uid).findFirst(); + if (affectedRouter.isPresent()) { + affectedRouter.get().maybeUpdateSystemRoutingPermissionLocked(); + } + } + } + // Start of methods that implement MediaRouter2 operations. @NonNull @@ -1511,6 +1542,7 @@ class MediaRouter2ServiceImpl { public final int mPid; public final boolean mHasConfigureWifiDisplayPermission; public final boolean mHasModifyAudioRoutingPermission; + public final AtomicBoolean mHasBluetoothRoutingPermission; public final int mRouterId; public RouteDiscoveryPreference mDiscoveryPreference; @@ -1528,15 +1560,47 @@ class MediaRouter2ServiceImpl { mPid = pid; mHasConfigureWifiDisplayPermission = hasConfigureWifiDisplayPermission; mHasModifyAudioRoutingPermission = hasModifyAudioRoutingPermission; + mHasBluetoothRoutingPermission = new AtomicBoolean(fetchBluetoothPermission()); mRouterId = mNextRouterOrManagerId.getAndIncrement(); } + private boolean fetchBluetoothPermission() { + boolean hasBluetoothRoutingPermission = true; + for (String permission : BLUETOOTH_PERMISSIONS_FOR_SYSTEM_ROUTING) { + hasBluetoothRoutingPermission &= + mContext.checkPermission(permission, mPid, mUid) + == PackageManager.PERMISSION_GRANTED; + } + return hasBluetoothRoutingPermission; + } + /** * Returns whether the corresponding router has permission to query and control system * routes. */ public boolean hasSystemRoutingPermission() { - return mHasModifyAudioRoutingPermission; + return mHasModifyAudioRoutingPermission || mHasBluetoothRoutingPermission.get(); + } + + public void maybeUpdateSystemRoutingPermissionLocked() { + boolean oldSystemRoutingPermissionValue = hasSystemRoutingPermission(); + mHasBluetoothRoutingPermission.set(fetchBluetoothPermission()); + boolean newSystemRoutingPermissionValue = hasSystemRoutingPermission(); + if (oldSystemRoutingPermissionValue != newSystemRoutingPermissionValue) { + Map routesToReport = + newSystemRoutingPermissionValue + ? mUserRecord.mHandler.mLastNotifiedRoutesToPrivilegedRouters + : mUserRecord.mHandler.mLastNotifiedRoutesToNonPrivilegedRouters; + notifyRoutesUpdated(routesToReport.values().stream().toList()); + + List sessionInfos = + mUserRecord.mHandler.mSystemProvider.getSessionInfos(); + RoutingSessionInfo systemSessionToReport = + newSystemRoutingPermissionValue && !sessionInfos.isEmpty() + ? sessionInfos.get(0) + : mUserRecord.mHandler.mSystemProvider.getDefaultSessionInfo(); + notifySessionInfoChanged(systemSessionToReport); + } } public void dispose() { @@ -1559,6 +1623,14 @@ class MediaRouter2ServiceImpl { pw.println(indent + "mPid=" + mPid); pw.println(indent + "mHasConfigureWifiDisplayPermission=" + mHasConfigureWifiDisplayPermission); + pw.println( + indent + + "mHasModifyAudioRoutingPermission=" + + mHasModifyAudioRoutingPermission); + pw.println( + indent + + "mHasBluetoothRoutingPermission=" + + mHasBluetoothRoutingPermission.get()); pw.println(indent + "hasSystemRoutingPermission=" + hasSystemRoutingPermission()); pw.println(indent + "mRouterId=" + mRouterId); @@ -1580,6 +1652,19 @@ class MediaRouter2ServiceImpl { } } + /** + * Sends the corresponding router an update for the given session. + * + *

Note: These updates are not directly visible to the app. + */ + public void notifySessionInfoChanged(RoutingSessionInfo sessionInfo) { + try { + mRouter.notifySessionInfoChanged(sessionInfo); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify session info changed. Router probably died.", ex); + } + } + /** * 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. @@ -2471,11 +2556,7 @@ class MediaRouter2ServiceImpl { @NonNull List routerRecords, @NonNull RoutingSessionInfo sessionInfo) { for (RouterRecord routerRecord : routerRecords) { - try { - routerRecord.mRouter.notifySessionInfoChanged(sessionInfo); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify session info changed. Router probably died.", ex); - } + routerRecord.notifySessionInfoChanged(sessionInfo); } } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 4d134b69811db..b440e8815c167 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -18,6 +18,7 @@ package com.android.server.media; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -137,6 +138,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub private final String mDefaultAudioRouteId; private final String mBluetoothA2dpRouteId; + @RequiresPermission(Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) public MediaRouterService(Context context) { mService2 = new MediaRouter2ServiceImpl(context); mContext = context; diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 6d2d2e405ab90..426bc5eed0515 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -55,7 +55,6 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { SystemMediaRoute2Provider.class.getPackage().getName(), SystemMediaRoute2Provider.class.getName()); - static final String DEFAULT_ROUTE_ID = "DEFAULT_ROUTE"; static final String SYSTEM_SESSION_ID = "SYSTEM_SESSION"; private final AudioManager mAudioManager; @@ -170,7 +169,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { Bundle sessionHints) { // 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, DEFAULT_ROUTE_ID)) { + if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { mCallback.onSessionCreated(this, requestId, mDefaultSessionInfo); return; } @@ -213,7 +212,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { @Override public void transferToRoute(long requestId, String sessionId, String routeId) { - if (TextUtils.equals(routeId, DEFAULT_ROUTE_ID)) { + if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { // The currently selected route is the default route. return; } @@ -326,10 +325,11 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { builder.addTransferableRoute(deviceRoute.getId()); } mSelectedRouteId = selectedRoute.getId(); - mDefaultRoute = new MediaRoute2Info.Builder(DEFAULT_ROUTE_ID, selectedRoute) - .setSystemRoute(true) - .setProviderId(mUniqueId) - .build(); + mDefaultRoute = + new MediaRoute2Info.Builder(MediaRoute2Info.ROUTE_ID_DEFAULT, selectedRoute) + .setSystemRoute(true) + .setProviderId(mUniqueId) + .build(); builder.addSelectedRoute(mSelectedRouteId); for (MediaRoute2Info route : mBluetoothRouteController.getTransferableRoutes()) { @@ -363,12 +363,13 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } mSessionInfos.clear(); mSessionInfos.add(newSessionInfo); - mDefaultSessionInfo = new RoutingSessionInfo.Builder( - SYSTEM_SESSION_ID, "" /* clientPackageName */) - .setProviderId(mUniqueId) - .setSystemSession(true) - .addSelectedRoute(DEFAULT_ROUTE_ID) - .build(); + mDefaultSessionInfo = + new RoutingSessionInfo.Builder( + SYSTEM_SESSION_ID, "" /* clientPackageName */) + .setProviderId(mUniqueId) + .setSystemSession(true) + .addSelectedRoute(MediaRoute2Info.ROUTE_ID_DEFAULT) + .build(); return true; } }