Allow apps with BLUETOOTH permission to control system routing
They can already control system routing via BluetoothManager, but it allows them to do both Remote and System routing via MediaRouter2. Concretely, this change is needed for apps to access the id of system routes via MediaRouter2 in order to add info to the output switcher via RouteListingPreference. Bug: 274894524 Test: atest CtsMediaBetterTogetherTestCases Test: atest MediaRouter2HostSideTest Test: atest AudioPoliciesDeviceRouteControllerTest LegacyDeviceRouteControllerTest Change-Id: If9bbfc7574e637aa4c07701edf1f2b12e27e4f52 Merged-In: If9bbfc7574e637aa4c07701edf1f2b12e27e4f52
This commit is contained in:
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<RouterRecord> 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<String, MediaRoute2Info> routesToReport =
|
||||
newSystemRoutingPermissionValue
|
||||
? mUserRecord.mHandler.mLastNotifiedRoutesToPrivilegedRouters
|
||||
: mUserRecord.mHandler.mLastNotifiedRoutesToNonPrivilegedRouters;
|
||||
notifyRoutesUpdated(routesToReport.values().stream().toList());
|
||||
|
||||
List<RoutingSessionInfo> 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.
|
||||
*
|
||||
* <p>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<RouterRecord> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user