Loading services/core/java/com/android/server/media/AudioManagerRouteController.java +40 −3 Original line number Diff line number Diff line Loading @@ -423,6 +423,31 @@ import java.util.concurrent.CopyOnWriteArrayList; return true; } @Override public void setVolume(long requestId, @NonNull String routeId, int volume) { mHandler.post(() -> setVolumeOnHandler(requestId, routeId, volume)); } private void setVolumeOnHandler(long requestId, @NonNull String routeId, int volume) { if (currentOutputIsBLEBroadcast()) { if (mBluetoothRouteController.isMediaOnlyRouteInBroadcast(routeId)) { // Media only device (device can only listen to broadcast source) volume // is controlled by volume control profile. boolean result = mBluetoothRouteController.setRouteVolume(routeId, volume); if (!result) { notifyRequestFailed( requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE); } } else { // Primary device (device can listen to call and broadcast source) // volume is bundled and controlled by music stream. mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); } } else { mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); } } private Runnable getTransferActionForRoute(MediaRoute2InfoHolder mediaRoute2InfoHolder) { if (mediaRoute2InfoHolder.mCorrespondsToInactiveBluetoothRoute) { String deviceAddress = mediaRoute2InfoHolder.mMediaRoute2Info.getAddress(); Loading Loading @@ -625,9 +650,21 @@ import java.util.concurrent.CopyOnWriteArrayList; List<MediaRoute2Info> newSelectedRoutes = new ArrayList<>(); for (MediaRoute2InfoHolder newSelectedRouteInfoHolderInBroadcast : newSelectedRouteInfoHoldersInBroadcast) { MediaRoute2InfoHolder selectedRouteHolderWithUpdatedVolumeInfo = MediaRoute2Info routeInfo = newSelectedRouteInfoHolderInBroadcast.mMediaRoute2Info; MediaRoute2InfoHolder selectedRouteHolderWithUpdatedVolumeInfo; if (routeInfo.getVolume() != BluetoothProfileMonitor.INVALID_VOLUME && routeInfo.getVolumeMax() == BluetoothProfileMonitor.MAXIMUM_DEVICE_VOLUME) { selectedRouteHolderWithUpdatedVolumeInfo = newSelectedRouteInfoHolderInBroadcast.copyWithVolumeInfo( routeInfo.getVolume(), routeInfo.getVolumeMax(), isVolumeFixed); } else { // Volume is not available from BT volume control profile, use music stream // volume by default. selectedRouteHolderWithUpdatedVolumeInfo = newSelectedRouteInfoHolderInBroadcast.copyWithVolumeInfo( musicVolume, musicMaxVolume, isVolumeFixed); } mRouteIdToAvailableDeviceRoutes.put( newSelectedRouteInfoHolderInBroadcast.mMediaRoute2Info.getId(), selectedRouteHolderWithUpdatedVolumeInfo); Loading services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java +56 −11 Original line number Diff line number Diff line Loading @@ -76,6 +76,13 @@ import java.util.stream.Collectors; void onBluetoothRoutesUpdated(); } /** Interface for receiving events about Broadcast sinks volume changes. */ interface OnBroadcastSinkVolumeChangedListener { /** Called when Bluetooth sink volume in broadcast has changed. */ void onBroadcastSinkVolumeChanged(); } @NonNull private final AdapterStateChangedReceiver mAdapterStateChangedReceiver = new AdapterStateChangedReceiver(); Loading Loading @@ -122,7 +129,7 @@ import java.util.stream.Collectors; public void start(UserHandle user, @NonNull BluetoothRoutesUpdatedListener listener) { mListener = listener; mBluetoothProfileMonitor.start(); mBluetoothProfileMonitor.start(() -> listener.onBluetoothRoutesUpdated()); IntentFilter adapterStateChangedIntentFilter = new IntentFilter(); Loading Loading @@ -179,6 +186,31 @@ import java.util.stream.Collectors; mBluetoothAdapter.setActiveDevice(btRouteInfo.mBtDevice, ACTIVE_DEVICE_AUDIO); } public synchronized boolean isMediaOnlyRouteInBroadcast(@NonNull String routeId) { for (BluetoothRouteInfo info : mBluetoothRoutes.values()) { if (info.mRoute.getId().equals(routeId)) { if (mBluetoothProfileMonitor.isMediaOnlyDeviceInBroadcast(info.mBtDevice)) { return true; } } } return false; } public synchronized boolean setRouteVolume(@NonNull String routeId, int volume) { boolean volumeUpdated = false; for (BluetoothRouteInfo info : mBluetoothRoutes.values()) { if (info.mRoute.getId().equals(routeId)) { // There could be multiple BT devices for the same route id, for example, LE Audio // devices, hearing aids. mBluetoothProfileMonitor.setDeviceVolume( info.mBtDevice, volume, /* isGroupOp= */ false); volumeUpdated = true; } } return volumeUpdated; } private void updateBluetoothRoutes() { Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices(); Loading @@ -199,7 +231,8 @@ import java.util.stream.Collectors; BluetoothDevice::getAddress, Function.identity())); for (BluetoothDevice device : bondedDevices) { if (device.isConnected()) { BluetoothRouteInfo newBtRoute = createBluetoothRoute(device); BluetoothRouteInfo newBtRoute = createBluetoothRoute(device, /* setVolume= */ false); if (newBtRoute.mConnectedProfiles.size() > 0) { mBluetoothRoutes.put(device.getAddress(), newBtRoute); } Loading Loading @@ -283,7 +316,13 @@ import java.util.stream.Collectors; // Convert List<BluetoothDevice> to List<MediaRoute2Info> return mBluetoothProfileMonitor.getDevicesWithBroadcastSource().stream() .map(device -> createBluetoothRoute(device).mRoute) .map( device -> createBluetoothRoute( device, /* setVolume= */ mBluetoothProfileMonitor .isMediaOnlyDeviceInBroadcast(device)) .mRoute) .filter(routeInfo -> routeIdSet.add(routeInfo.getId())) .toList(); } Loading @@ -307,9 +346,8 @@ import java.util.stream.Collectors; * bluetooth devices individually, since the audio stack refers to a bluetooth device group by * any of its member devices. */ private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device) { BluetoothRouteInfo newBtRoute = new BluetoothRouteInfo(); private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device, boolean setVolume) { BluetoothRouteInfo newBtRoute = new BluetoothRouteInfo(); newBtRoute.mBtDevice = device; String deviceName = getDeviceName(device); Loading @@ -317,9 +355,7 @@ import java.util.stream.Collectors; String routeId = getRouteIdForType(device, type); newBtRoute.mConnectedProfiles = getConnectedProfiles(device); // Note that volume is only relevant for active bluetooth routes, and those are managed via // AudioManager. newBtRoute.mRoute = MediaRoute2Info.Builder routeInfoBuilder = new MediaRoute2Info.Builder(routeId, deviceName) .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO) .addFeature(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK) Loading @@ -329,8 +365,17 @@ import java.util.stream.Collectors; .getText(R.string.bluetooth_a2dp_audio_route_name) .toString()) .setType(type) .setAddress(device.getAddress()) .build(); .setAddress(device.getAddress()); // Note that volume is only relevant for active bluetooth routes, and those are managed via // AudioManager. // The only exception is media only devices in broadcast, the volume is fetched from // bluetooth volume control profile. if (setVolume) { routeInfoBuilder .setVolume(mBluetoothProfileMonitor.getDeviceVolume(device)) .setVolumeMax(BluetoothProfileMonitor.MAXIMUM_DEVICE_VOLUME); } newBtRoute.mRoute = routeInfoBuilder.build(); return newBtRoute; } Loading services/core/java/com/android/server/media/BluetoothProfileMonitor.java +114 −7 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastSettings; import android.bluetooth.BluetoothLeBroadcastSubgroupSettings; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothVolumeControl; import android.content.ContentResolver; import android.content.Context; import android.os.Handler; Loading @@ -46,6 +47,7 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; /* package */ class BluetoothProfileMonitor { Loading @@ -53,7 +55,14 @@ import java.util.concurrent.ThreadLocalRandom; private static final String TAG = BluetoothProfileMonitor.class.getSimpleName(); /* package */ static final long GROUP_ID_NO_GROUP = -1L; /* package */ static final int INVALID_VOLUME = -1; /* package */ static final int MAXIMUM_DEVICE_VOLUME = 255; // TODO(b/397568136): remove reading primary group id from SettingsProvider once // adopt_primary_group_management_api_v2 is rolled out private static final String KEY_PRIMARY_GROUP_ID = "bluetooth_le_broadcast_fallback_active_group_id"; private static final int INVALID_BROADCAST_ID = 0; private static final String UNDERLINE = "_"; private static final int DEFAULT_CODE_MAX = 9999; private static final int DEFAULT_CODE_MIN = 1000; Loading @@ -69,6 +78,7 @@ import java.util.concurrent.ThreadLocalRandom; @NonNull private final ProfileListener mProfileListener = new ProfileListener(); @NonNull private final BroadcastCallback mBroadcastCallback = new BroadcastCallback(); @NonNull private final VolumeControlCallback mVolumeCallback = new VolumeControlCallback(); @NonNull private final BroadcastAssistantCallback mBroadcastAssistantCallback = Loading @@ -86,11 +96,20 @@ import java.util.concurrent.ThreadLocalRandom; private BluetoothLeAudio mLeAudioProfile; @GuardedBy("this") @Nullable private BluetoothLeBroadcast mBroadcastProfile; private BluetoothLeBroadcastAssistant mAssistantProfile; @Nullable private BluetoothLeBroadcastAssistant mAssistantProfile; @Nullable private BluetoothVolumeControl mVolumeProfile; private final List<BluetoothDevice> mDevicesToAdd = new ArrayList<>(); private int mBroadcastId = 0; private int mBroadcastId = INVALID_BROADCAST_ID; private final ConcurrentHashMap<BluetoothDevice, Integer> mVolumeMap = new ConcurrentHashMap<>(); @NonNull private BluetoothDeviceRoutesManager.OnBroadcastSinkVolumeChangedListener mVolumeChangedListener; BluetoothProfileMonitor( @NonNull Context context, Loading @@ -99,9 +118,15 @@ import java.util.concurrent.ThreadLocalRandom; mContext = Objects.requireNonNull(context); mHandler = new Handler(Objects.requireNonNull(looper)); mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter); // no-op listener, will be overridden in start() mVolumeChangedListener = () -> {}; } /* package */ void start() { /* package */ void start( @NonNull BluetoothDeviceRoutesManager.OnBroadcastSinkVolumeChangedListener volumeListener) { mVolumeChangedListener = volumeListener; mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEARING_AID); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.LE_AUDIO); Loading @@ -110,6 +135,8 @@ import java.util.concurrent.ThreadLocalRandom; mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST); mBluetoothAdapter.getProfileProxy( mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); mBluetoothAdapter.getProfileProxy( mContext, mProfileListener, BluetoothProfile.VOLUME_CONTROL); } } Loading @@ -128,8 +155,8 @@ import java.util.concurrent.ThreadLocalRandom; bluetoothProfile = mHearingAidProfile; break; default: throw new IllegalArgumentException(profile + " is not supported as Bluetooth profile"); throw new IllegalArgumentException( profile + " is not supported as Bluetooth profile"); } } Loading Loading @@ -391,6 +418,64 @@ import java.util.concurrent.ThreadLocalRandom; .build(); } /* package */ boolean isDeviceInBroadcast(@NonNull BluetoothDevice device) { return mAssistantProfile != null && mBroadcastId != INVALID_BROADCAST_ID && mAssistantProfile.getAllSources(device).stream() .anyMatch(source -> source.getBroadcastId() == mBroadcastId); } /* package */ void setDeviceVolume( @NonNull BluetoothDevice device, int volume, boolean isGroupOp) { if (mVolumeProfile != null) { mVolumeProfile.setDeviceVolume(device, volume, isGroupOp); } } /* package */ int getDeviceVolume(@NonNull BluetoothDevice device) { return isDeviceInBroadcast(device) ? mVolumeMap.getOrDefault(device, INVALID_VOLUME) : INVALID_VOLUME; } /** * Check if the BT device is the media only device in broadcast. * * <p>There are two types of sinks in the broadcast session, primary sink and media only sink. * * <p>Primary sink is the sink can listen to the call, usually it is the one belongs to the * broadcast owner. * * <p>Media only sink can only listen to audio shared by the broadcaster, including media and * notification. */ /* package */ boolean isMediaOnlyDeviceInBroadcast(@NonNull BluetoothDevice device) { // Media only device, other than primary device, can only listen to the broadcast content // and is not the default one to listen to the call. long groupId = getGroupId(BluetoothProfile.LE_AUDIO, device); if (groupId == GROUP_ID_NO_GROUP) { Slog.d(TAG, "isMediaOnlyDeviceInBroadcast, invalid group id"); return false; } long primaryGroupId = GROUP_ID_NO_GROUP; if (com.android.settingslib.flags.Flags.adoptPrimaryGroupManagementApiV2()) { if (mLeAudioProfile != null) { primaryGroupId = mLeAudioProfile.getBroadcastToUnicastFallbackGroup(); } } else { // TODO(b/397568136): remove reading primary group id from SettingsProvider once // adopt_primary_group_management_api_v2 is rolled out ContentResolver contentResolver = mContext.getContentResolver(); primaryGroupId = Settings.Secure.getIntForUser( contentResolver, KEY_PRIMARY_GROUP_ID, (int) GROUP_ID_NO_GROUP, contentResolver.getUserId()); } return groupId != primaryGroupId; } private final class ProfileListener implements BluetoothProfile.ServiceListener { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { Loading Loading @@ -418,6 +503,12 @@ import java.util.concurrent.ThreadLocalRandom; mHandler::post, mBroadcastAssistantCallback); } break; case BluetoothProfile.VOLUME_CONTROL: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mVolumeProfile = (BluetoothVolumeControl) proxy; mVolumeProfile.registerCallback(mHandler::post, mVolumeCallback); } break; } } } Loading @@ -439,7 +530,7 @@ import java.util.concurrent.ThreadLocalRandom; if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mBroadcastProfile.unregisterCallback(mBroadcastCallback); mBroadcastProfile = null; mBroadcastId = 0; mBroadcastId = INVALID_BROADCAST_ID; } break; case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT: Loading @@ -448,6 +539,12 @@ import java.util.concurrent.ThreadLocalRandom; mAssistantProfile = null; } break; case BluetoothProfile.VOLUME_CONTROL: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mVolumeProfile.unregisterCallback(mVolumeCallback); mVolumeProfile = null; } break; } } } Loading Loading @@ -537,4 +634,14 @@ import java.util.concurrent.ThreadLocalRandom; int sourceId, @NonNull BluetoothLeBroadcastReceiveState state) {} } private final class VolumeControlCallback implements BluetoothVolumeControl.Callback { @Override public void onDeviceVolumeChanged(@NonNull BluetoothDevice device, int volume) { mVolumeMap.put(device, volume); if (isMediaOnlyDeviceInBroadcast(device)) { mVolumeChangedListener.onBroadcastSinkVolumeChanged(); } } } } services/core/java/com/android/server/media/DeviceRouteController.java +9 −0 Original line number Diff line number Diff line Loading @@ -167,6 +167,15 @@ import java.util.List; */ boolean updateVolume(int volume); /** * Sets device route volume. * * @param requestId identifies the request. * @param routeId to set the volume. * @param volume specifies a volume for the device route. */ void setVolume(long requestId, @NonNull String routeId, int volume); /** * Starts listening for changes in the system to keep an up to date view of available and * selected devices. Loading services/core/java/com/android/server/media/LegacyDeviceRouteController.java +5 −0 Original line number Diff line number Diff line Loading @@ -181,6 +181,11 @@ import java.util.Objects; return true; } @Override public synchronized void setVolume(long requestId, @NonNull String routeId, int volume) { // Do nothing } private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) { int name = R.string.default_audio_route_name; int type = TYPE_BUILTIN_SPEAKER; Loading Loading
services/core/java/com/android/server/media/AudioManagerRouteController.java +40 −3 Original line number Diff line number Diff line Loading @@ -423,6 +423,31 @@ import java.util.concurrent.CopyOnWriteArrayList; return true; } @Override public void setVolume(long requestId, @NonNull String routeId, int volume) { mHandler.post(() -> setVolumeOnHandler(requestId, routeId, volume)); } private void setVolumeOnHandler(long requestId, @NonNull String routeId, int volume) { if (currentOutputIsBLEBroadcast()) { if (mBluetoothRouteController.isMediaOnlyRouteInBroadcast(routeId)) { // Media only device (device can only listen to broadcast source) volume // is controlled by volume control profile. boolean result = mBluetoothRouteController.setRouteVolume(routeId, volume); if (!result) { notifyRequestFailed( requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE); } } else { // Primary device (device can listen to call and broadcast source) // volume is bundled and controlled by music stream. mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); } } else { mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); } } private Runnable getTransferActionForRoute(MediaRoute2InfoHolder mediaRoute2InfoHolder) { if (mediaRoute2InfoHolder.mCorrespondsToInactiveBluetoothRoute) { String deviceAddress = mediaRoute2InfoHolder.mMediaRoute2Info.getAddress(); Loading Loading @@ -625,9 +650,21 @@ import java.util.concurrent.CopyOnWriteArrayList; List<MediaRoute2Info> newSelectedRoutes = new ArrayList<>(); for (MediaRoute2InfoHolder newSelectedRouteInfoHolderInBroadcast : newSelectedRouteInfoHoldersInBroadcast) { MediaRoute2InfoHolder selectedRouteHolderWithUpdatedVolumeInfo = MediaRoute2Info routeInfo = newSelectedRouteInfoHolderInBroadcast.mMediaRoute2Info; MediaRoute2InfoHolder selectedRouteHolderWithUpdatedVolumeInfo; if (routeInfo.getVolume() != BluetoothProfileMonitor.INVALID_VOLUME && routeInfo.getVolumeMax() == BluetoothProfileMonitor.MAXIMUM_DEVICE_VOLUME) { selectedRouteHolderWithUpdatedVolumeInfo = newSelectedRouteInfoHolderInBroadcast.copyWithVolumeInfo( routeInfo.getVolume(), routeInfo.getVolumeMax(), isVolumeFixed); } else { // Volume is not available from BT volume control profile, use music stream // volume by default. selectedRouteHolderWithUpdatedVolumeInfo = newSelectedRouteInfoHolderInBroadcast.copyWithVolumeInfo( musicVolume, musicMaxVolume, isVolumeFixed); } mRouteIdToAvailableDeviceRoutes.put( newSelectedRouteInfoHolderInBroadcast.mMediaRoute2Info.getId(), selectedRouteHolderWithUpdatedVolumeInfo); Loading
services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java +56 −11 Original line number Diff line number Diff line Loading @@ -76,6 +76,13 @@ import java.util.stream.Collectors; void onBluetoothRoutesUpdated(); } /** Interface for receiving events about Broadcast sinks volume changes. */ interface OnBroadcastSinkVolumeChangedListener { /** Called when Bluetooth sink volume in broadcast has changed. */ void onBroadcastSinkVolumeChanged(); } @NonNull private final AdapterStateChangedReceiver mAdapterStateChangedReceiver = new AdapterStateChangedReceiver(); Loading Loading @@ -122,7 +129,7 @@ import java.util.stream.Collectors; public void start(UserHandle user, @NonNull BluetoothRoutesUpdatedListener listener) { mListener = listener; mBluetoothProfileMonitor.start(); mBluetoothProfileMonitor.start(() -> listener.onBluetoothRoutesUpdated()); IntentFilter adapterStateChangedIntentFilter = new IntentFilter(); Loading Loading @@ -179,6 +186,31 @@ import java.util.stream.Collectors; mBluetoothAdapter.setActiveDevice(btRouteInfo.mBtDevice, ACTIVE_DEVICE_AUDIO); } public synchronized boolean isMediaOnlyRouteInBroadcast(@NonNull String routeId) { for (BluetoothRouteInfo info : mBluetoothRoutes.values()) { if (info.mRoute.getId().equals(routeId)) { if (mBluetoothProfileMonitor.isMediaOnlyDeviceInBroadcast(info.mBtDevice)) { return true; } } } return false; } public synchronized boolean setRouteVolume(@NonNull String routeId, int volume) { boolean volumeUpdated = false; for (BluetoothRouteInfo info : mBluetoothRoutes.values()) { if (info.mRoute.getId().equals(routeId)) { // There could be multiple BT devices for the same route id, for example, LE Audio // devices, hearing aids. mBluetoothProfileMonitor.setDeviceVolume( info.mBtDevice, volume, /* isGroupOp= */ false); volumeUpdated = true; } } return volumeUpdated; } private void updateBluetoothRoutes() { Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices(); Loading @@ -199,7 +231,8 @@ import java.util.stream.Collectors; BluetoothDevice::getAddress, Function.identity())); for (BluetoothDevice device : bondedDevices) { if (device.isConnected()) { BluetoothRouteInfo newBtRoute = createBluetoothRoute(device); BluetoothRouteInfo newBtRoute = createBluetoothRoute(device, /* setVolume= */ false); if (newBtRoute.mConnectedProfiles.size() > 0) { mBluetoothRoutes.put(device.getAddress(), newBtRoute); } Loading Loading @@ -283,7 +316,13 @@ import java.util.stream.Collectors; // Convert List<BluetoothDevice> to List<MediaRoute2Info> return mBluetoothProfileMonitor.getDevicesWithBroadcastSource().stream() .map(device -> createBluetoothRoute(device).mRoute) .map( device -> createBluetoothRoute( device, /* setVolume= */ mBluetoothProfileMonitor .isMediaOnlyDeviceInBroadcast(device)) .mRoute) .filter(routeInfo -> routeIdSet.add(routeInfo.getId())) .toList(); } Loading @@ -307,9 +346,8 @@ import java.util.stream.Collectors; * bluetooth devices individually, since the audio stack refers to a bluetooth device group by * any of its member devices. */ private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device) { BluetoothRouteInfo newBtRoute = new BluetoothRouteInfo(); private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device, boolean setVolume) { BluetoothRouteInfo newBtRoute = new BluetoothRouteInfo(); newBtRoute.mBtDevice = device; String deviceName = getDeviceName(device); Loading @@ -317,9 +355,7 @@ import java.util.stream.Collectors; String routeId = getRouteIdForType(device, type); newBtRoute.mConnectedProfiles = getConnectedProfiles(device); // Note that volume is only relevant for active bluetooth routes, and those are managed via // AudioManager. newBtRoute.mRoute = MediaRoute2Info.Builder routeInfoBuilder = new MediaRoute2Info.Builder(routeId, deviceName) .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO) .addFeature(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK) Loading @@ -329,8 +365,17 @@ import java.util.stream.Collectors; .getText(R.string.bluetooth_a2dp_audio_route_name) .toString()) .setType(type) .setAddress(device.getAddress()) .build(); .setAddress(device.getAddress()); // Note that volume is only relevant for active bluetooth routes, and those are managed via // AudioManager. // The only exception is media only devices in broadcast, the volume is fetched from // bluetooth volume control profile. if (setVolume) { routeInfoBuilder .setVolume(mBluetoothProfileMonitor.getDeviceVolume(device)) .setVolumeMax(BluetoothProfileMonitor.MAXIMUM_DEVICE_VOLUME); } newBtRoute.mRoute = routeInfoBuilder.build(); return newBtRoute; } Loading
services/core/java/com/android/server/media/BluetoothProfileMonitor.java +114 −7 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastSettings; import android.bluetooth.BluetoothLeBroadcastSubgroupSettings; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothVolumeControl; import android.content.ContentResolver; import android.content.Context; import android.os.Handler; Loading @@ -46,6 +47,7 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; /* package */ class BluetoothProfileMonitor { Loading @@ -53,7 +55,14 @@ import java.util.concurrent.ThreadLocalRandom; private static final String TAG = BluetoothProfileMonitor.class.getSimpleName(); /* package */ static final long GROUP_ID_NO_GROUP = -1L; /* package */ static final int INVALID_VOLUME = -1; /* package */ static final int MAXIMUM_DEVICE_VOLUME = 255; // TODO(b/397568136): remove reading primary group id from SettingsProvider once // adopt_primary_group_management_api_v2 is rolled out private static final String KEY_PRIMARY_GROUP_ID = "bluetooth_le_broadcast_fallback_active_group_id"; private static final int INVALID_BROADCAST_ID = 0; private static final String UNDERLINE = "_"; private static final int DEFAULT_CODE_MAX = 9999; private static final int DEFAULT_CODE_MIN = 1000; Loading @@ -69,6 +78,7 @@ import java.util.concurrent.ThreadLocalRandom; @NonNull private final ProfileListener mProfileListener = new ProfileListener(); @NonNull private final BroadcastCallback mBroadcastCallback = new BroadcastCallback(); @NonNull private final VolumeControlCallback mVolumeCallback = new VolumeControlCallback(); @NonNull private final BroadcastAssistantCallback mBroadcastAssistantCallback = Loading @@ -86,11 +96,20 @@ import java.util.concurrent.ThreadLocalRandom; private BluetoothLeAudio mLeAudioProfile; @GuardedBy("this") @Nullable private BluetoothLeBroadcast mBroadcastProfile; private BluetoothLeBroadcastAssistant mAssistantProfile; @Nullable private BluetoothLeBroadcastAssistant mAssistantProfile; @Nullable private BluetoothVolumeControl mVolumeProfile; private final List<BluetoothDevice> mDevicesToAdd = new ArrayList<>(); private int mBroadcastId = 0; private int mBroadcastId = INVALID_BROADCAST_ID; private final ConcurrentHashMap<BluetoothDevice, Integer> mVolumeMap = new ConcurrentHashMap<>(); @NonNull private BluetoothDeviceRoutesManager.OnBroadcastSinkVolumeChangedListener mVolumeChangedListener; BluetoothProfileMonitor( @NonNull Context context, Loading @@ -99,9 +118,15 @@ import java.util.concurrent.ThreadLocalRandom; mContext = Objects.requireNonNull(context); mHandler = new Handler(Objects.requireNonNull(looper)); mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter); // no-op listener, will be overridden in start() mVolumeChangedListener = () -> {}; } /* package */ void start() { /* package */ void start( @NonNull BluetoothDeviceRoutesManager.OnBroadcastSinkVolumeChangedListener volumeListener) { mVolumeChangedListener = volumeListener; mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEARING_AID); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.LE_AUDIO); Loading @@ -110,6 +135,8 @@ import java.util.concurrent.ThreadLocalRandom; mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST); mBluetoothAdapter.getProfileProxy( mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); mBluetoothAdapter.getProfileProxy( mContext, mProfileListener, BluetoothProfile.VOLUME_CONTROL); } } Loading @@ -128,8 +155,8 @@ import java.util.concurrent.ThreadLocalRandom; bluetoothProfile = mHearingAidProfile; break; default: throw new IllegalArgumentException(profile + " is not supported as Bluetooth profile"); throw new IllegalArgumentException( profile + " is not supported as Bluetooth profile"); } } Loading Loading @@ -391,6 +418,64 @@ import java.util.concurrent.ThreadLocalRandom; .build(); } /* package */ boolean isDeviceInBroadcast(@NonNull BluetoothDevice device) { return mAssistantProfile != null && mBroadcastId != INVALID_BROADCAST_ID && mAssistantProfile.getAllSources(device).stream() .anyMatch(source -> source.getBroadcastId() == mBroadcastId); } /* package */ void setDeviceVolume( @NonNull BluetoothDevice device, int volume, boolean isGroupOp) { if (mVolumeProfile != null) { mVolumeProfile.setDeviceVolume(device, volume, isGroupOp); } } /* package */ int getDeviceVolume(@NonNull BluetoothDevice device) { return isDeviceInBroadcast(device) ? mVolumeMap.getOrDefault(device, INVALID_VOLUME) : INVALID_VOLUME; } /** * Check if the BT device is the media only device in broadcast. * * <p>There are two types of sinks in the broadcast session, primary sink and media only sink. * * <p>Primary sink is the sink can listen to the call, usually it is the one belongs to the * broadcast owner. * * <p>Media only sink can only listen to audio shared by the broadcaster, including media and * notification. */ /* package */ boolean isMediaOnlyDeviceInBroadcast(@NonNull BluetoothDevice device) { // Media only device, other than primary device, can only listen to the broadcast content // and is not the default one to listen to the call. long groupId = getGroupId(BluetoothProfile.LE_AUDIO, device); if (groupId == GROUP_ID_NO_GROUP) { Slog.d(TAG, "isMediaOnlyDeviceInBroadcast, invalid group id"); return false; } long primaryGroupId = GROUP_ID_NO_GROUP; if (com.android.settingslib.flags.Flags.adoptPrimaryGroupManagementApiV2()) { if (mLeAudioProfile != null) { primaryGroupId = mLeAudioProfile.getBroadcastToUnicastFallbackGroup(); } } else { // TODO(b/397568136): remove reading primary group id from SettingsProvider once // adopt_primary_group_management_api_v2 is rolled out ContentResolver contentResolver = mContext.getContentResolver(); primaryGroupId = Settings.Secure.getIntForUser( contentResolver, KEY_PRIMARY_GROUP_ID, (int) GROUP_ID_NO_GROUP, contentResolver.getUserId()); } return groupId != primaryGroupId; } private final class ProfileListener implements BluetoothProfile.ServiceListener { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { Loading Loading @@ -418,6 +503,12 @@ import java.util.concurrent.ThreadLocalRandom; mHandler::post, mBroadcastAssistantCallback); } break; case BluetoothProfile.VOLUME_CONTROL: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mVolumeProfile = (BluetoothVolumeControl) proxy; mVolumeProfile.registerCallback(mHandler::post, mVolumeCallback); } break; } } } Loading @@ -439,7 +530,7 @@ import java.util.concurrent.ThreadLocalRandom; if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mBroadcastProfile.unregisterCallback(mBroadcastCallback); mBroadcastProfile = null; mBroadcastId = 0; mBroadcastId = INVALID_BROADCAST_ID; } break; case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT: Loading @@ -448,6 +539,12 @@ import java.util.concurrent.ThreadLocalRandom; mAssistantProfile = null; } break; case BluetoothProfile.VOLUME_CONTROL: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mVolumeProfile.unregisterCallback(mVolumeCallback); mVolumeProfile = null; } break; } } } Loading Loading @@ -537,4 +634,14 @@ import java.util.concurrent.ThreadLocalRandom; int sourceId, @NonNull BluetoothLeBroadcastReceiveState state) {} } private final class VolumeControlCallback implements BluetoothVolumeControl.Callback { @Override public void onDeviceVolumeChanged(@NonNull BluetoothDevice device, int volume) { mVolumeMap.put(device, volume); if (isMediaOnlyDeviceInBroadcast(device)) { mVolumeChangedListener.onBroadcastSinkVolumeChanged(); } } } }
services/core/java/com/android/server/media/DeviceRouteController.java +9 −0 Original line number Diff line number Diff line Loading @@ -167,6 +167,15 @@ import java.util.List; */ boolean updateVolume(int volume); /** * Sets device route volume. * * @param requestId identifies the request. * @param routeId to set the volume. * @param volume specifies a volume for the device route. */ void setVolume(long requestId, @NonNull String routeId, int volume); /** * Starts listening for changes in the system to keep an up to date view of available and * selected devices. Loading
services/core/java/com/android/server/media/LegacyDeviceRouteController.java +5 −0 Original line number Diff line number Diff line Loading @@ -181,6 +181,11 @@ import java.util.Objects; return true; } @Override public synchronized void setVolume(long requestId, @NonNull String routeId, int volume) { // Do nothing } private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) { int name = R.string.default_audio_route_name; int type = TYPE_BUILTIN_SPEAKER; Loading