Loading services/core/java/com/android/server/media/AudioManagerRouteController.java +5 −1 Original line number Diff line number Diff line Loading @@ -177,7 +177,11 @@ import java.util.concurrent.CopyOnWriteArrayList; mBluetoothRouteController = new BluetoothDeviceRoutesManager( mContext, mHandler, btAdapter, this::rebuildAvailableRoutesAndNotify); mContext, mHandler, looper, btAdapter, this::rebuildAvailableRoutesAndNotify); // Just build routes but don't notify. The caller may not expect the listener to be invoked // before this constructor has finished executing. rebuildAvailableRoutes(); Loading services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java +3 −1 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.media.MediaRoute2Info; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; Loading Loading @@ -94,13 +95,14 @@ import java.util.stream.Collectors; BluetoothDeviceRoutesManager( @NonNull Context context, @NonNull Handler handler, @NonNull Looper looper, @NonNull BluetoothAdapter bluetoothAdapter, @NonNull BluetoothRoutesUpdatedListener listener) { this( context, handler, bluetoothAdapter, new BluetoothProfileMonitor(context, bluetoothAdapter), new BluetoothProfileMonitor(context, looper, bluetoothAdapter), listener); } Loading services/core/java/com/android/server/media/BluetoothProfileMonitor.java +403 −4 Original line number Diff line number Diff line Loading @@ -23,22 +23,60 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothLeAudioContentMetadata; import android.bluetooth.BluetoothLeBroadcast; import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastSettings; import android.bluetooth.BluetoothLeBroadcastSubgroupSettings; import android.bluetooth.BluetoothProfile; import android.content.ContentResolver; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.media.flags.Flags; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ThreadLocalRandom; /* package */ class BluetoothProfileMonitor { private static final String TAG = BluetoothProfileMonitor.class.getSimpleName(); /* package */ static final long GROUP_ID_NO_GROUP = -1L; private static final String UNDERLINE = "_"; private static final int DEFAULT_CODE_MAX = 9999; private static final int DEFAULT_CODE_MIN = 1000; private static final int MIN_NO_DEVICES_FOR_BROADCAST = 2; private static final String VALID_PASSWORD_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:," + ".<>?/"; private static final int PASSWORD_LENGTH = 16; private static final int BROADCAST_NAME_PREFIX_MAX_LENGTH = 27; private static final String DEFAULT_BROADCAST_NAME_PREFIX = "Broadcast"; private static final String IMPROVE_COMPATIBILITY_HIGH_QUALITY = "1"; @NonNull private final ProfileListener mProfileListener = new ProfileListener(); @NonNull private final BroadcastCallback mBroadcastCallback = new BroadcastCallback(); @NonNull private final Context mContext; @NonNull private final BluetoothAdapter mBluetoothAdapter; private final BroadcastAssistantCallback mBroadcastAssistantCallback = new BroadcastAssistantCallback(); @NonNull private final Context mContext; @NonNull private final Handler mHandler; @NonNull private final BluetoothAdapter mBluetoothAdapter; @Nullable private BluetoothA2dp mA2dpProfile; Loading @@ -47,9 +85,19 @@ import java.util.Objects; @Nullable private BluetoothLeAudio mLeAudioProfile; BluetoothProfileMonitor(@NonNull Context context, @GuardedBy("this") private BluetoothLeBroadcast mBroadcastProfile; private BluetoothLeBroadcastAssistant mAssistantProfile; private final List<BluetoothDevice> mDevicesToAdd = new ArrayList<>(); private int mBroadcastId = 0; BluetoothProfileMonitor( @NonNull Context context, @NonNull Looper looper, @NonNull BluetoothAdapter bluetoothAdapter) { mContext = Objects.requireNonNull(context); mHandler = new Handler(Objects.requireNonNull(looper)); mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter); } Loading @@ -57,6 +105,12 @@ import java.util.Objects; mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEARING_AID); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.LE_AUDIO); if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mBluetoothAdapter.getProfileProxy( mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST); mBluetoothAdapter.getProfileProxy( mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); } } /* package */ boolean isProfileSupported(int profile, @NonNull BluetoothDevice device) { Loading Loading @@ -104,6 +158,240 @@ import java.util.Objects; } } /** * Starts broadcast and connect to given bluetooth devices. * * @param devices Bluetooth devices that are going to connect the to broadcast */ public synchronized void startBroadcast(List<BluetoothDevice> devices) { if (devices.size() < MIN_NO_DEVICES_FOR_BROADCAST) { Slog.e( TAG, "Insufficient number of device to start broadcast: need to have at least " + MIN_NO_DEVICES_FOR_BROADCAST + " device(s) to start broadcast, current no. of selected device(s) = " + devices.size()); return; } if (mBroadcastProfile == null) { // BluetoothLeBroadcast is not initialized properly Slog.e(TAG, "Fail to start broadcast, LeBroadcast is null"); return; } if (mBroadcastProfile.getAllBroadcastMetadata().size() >= getMaximumNumberOfBroadcasts()) { Slog.e( TAG, "Fail to start broadcast, current number of broadcasting group: " + mBroadcastProfile.getAllBroadcastMetadata().size() + ", exceeds the maximum allowed: " + getMaximumNumberOfBroadcasts()); return; } // Store the broadcast name so that program info and broadcast setting can use the same // value. String broadcastName = getBroadcastName(); // Current broadcast framework only support one subgroup BluetoothLeBroadcastSubgroupSettings subgroupSettings = buildBroadcastSubgroupSettings( /* language= */ null, getProgramInfo(broadcastName), isImproveQualityFlagEnabled()); BluetoothLeBroadcastSettings settings = buildBroadcastSettings( /* isPublic= */ true, // TODO(b/421062071): Set to false after framework // fix. broadcastName, getBroadcastCode(), List.of(subgroupSettings)); mDevicesToAdd.clear(); mDevicesToAdd.addAll(devices); mBroadcastProfile.startBroadcast(settings); } /** Stops the broadcast. */ public synchronized void stopBroadcast() { if (mBroadcastProfile != null) { mBroadcastProfile.stopBroadcast(mBroadcastId); } else { Slog.e(TAG, "Fail to stop broadcast, LeBroadcast is null"); } } /** * Obtains selected bluetooth devices from broadcast assistant that are broadcasting. * * @return list of selected {@link BluetoothDevice} */ public List<BluetoothDevice> getDevicesWithBroadcastSource() { if (mAssistantProfile == null) { return List.of(); } return mAssistantProfile.getConnectedDevices().stream() .filter( device -> mAssistantProfile.getAllSources(device).stream() .anyMatch( source -> source.getBroadcastId() == mBroadcastId)) .toList(); } /** * Gets the maximum number of Broadcast Isochronous Group supported on this device from * broadcast profile * * @return value of the maximum number of Broadcast Isochronous Group supported on this device * from broadcast profile */ public int getMaximumNumberOfBroadcasts() { return mBroadcastProfile.getMaximumNumberOfBroadcasts(); } /** * Perform add device as broadcast source to {@link BluetoothLeBroadcastAssistant}. Devices will * then receive audio broadcast * * @param deviceList - List of {@link BluetoothDevice} for broadcast * @param metadata - broadcast metadata that obtained from broadcast object */ private void addSourceToDevices( List<BluetoothDevice> deviceList, BluetoothLeBroadcastMetadata metadata) { if (mAssistantProfile == null) { Slog.d(TAG, "BroadcastAssistant is null"); return; } if (metadata == null) { Slog.d(TAG, "BroadcastMetadata is null"); return; } for (BluetoothDevice device : deviceList) { mAssistantProfile.addSource(device, metadata, /* isGroupOp= */ false); } } @Nullable private byte[] getBroadcastCode() { ContentResolver contentResolver = mContext.getContentResolver(); String prefBroadcastCode = Settings.Secure.getStringForUser( contentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE, contentResolver.getUserId()); byte[] broadcastCode = (prefBroadcastCode == null) ? generateRandomPassword().getBytes(StandardCharsets.UTF_8) : prefBroadcastCode.getBytes(StandardCharsets.UTF_8); return (broadcastCode != null && broadcastCode.length > 0) ? broadcastCode : null; } @NonNull private String getBroadcastName() { ContentResolver contentResolver = mContext.getContentResolver(); String settingBroadcastName = Settings.Secure.getStringForUser( contentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME, contentResolver.getUserId()); if (settingBroadcastName == null || settingBroadcastName.isEmpty()) { int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX); String name = BluetoothAdapter.getDefaultAdapter().getName(); if (name == null || name.isEmpty()) { name = DEFAULT_BROADCAST_NAME_PREFIX; } return (name.length() < BROADCAST_NAME_PREFIX_MAX_LENGTH ? name : name.substring(0, BROADCAST_NAME_PREFIX_MAX_LENGTH)) + UNDERLINE + postfix; } return settingBroadcastName; } @NonNull private String getProgramInfo(@NonNull String defaultProgramInfo) { ContentResolver contentResolver = mContext.getContentResolver(); String programInfo = Settings.Secure.getStringForUser( contentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, contentResolver.getUserId()); if (programInfo == null || programInfo.isEmpty()) { return defaultProgramInfo; } return programInfo; } private static String generateRandomPassword() { SecureRandom random = new SecureRandom(); StringBuilder stringBuilder = new StringBuilder(PASSWORD_LENGTH); for (int i = 0; i < PASSWORD_LENGTH; i++) { int randomIndex = random.nextInt(VALID_PASSWORD_CHARACTERS.length()); stringBuilder.append(VALID_PASSWORD_CHARACTERS.charAt(randomIndex)); } return stringBuilder.toString(); } private boolean isImproveQualityFlagEnabled() { ContentResolver contentResolver = mContext.getContentResolver(); // BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY takes only "1" and "0" in string only. Check // android.provider.settings.validators.SecureSettingsValidators for mode details. return IMPROVE_COMPATIBILITY_HIGH_QUALITY.equals( Settings.Secure.getStringForUser( contentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY, contentResolver.getUserId())); } private BluetoothLeBroadcastSettings buildBroadcastSettings( boolean isPublic, String broadcastName, byte[] broadcastCode, List<BluetoothLeBroadcastSubgroupSettings> subgroupSettingsList) { BluetoothLeBroadcastSettings.Builder builder = new BluetoothLeBroadcastSettings.Builder() .setPublicBroadcast(isPublic) .setBroadcastName(broadcastName) .setBroadcastCode(broadcastCode); for (BluetoothLeBroadcastSubgroupSettings subgroupSettings : subgroupSettingsList) { builder.addSubgroupSettings(subgroupSettings); } return builder.build(); } private BluetoothLeBroadcastSubgroupSettings buildBroadcastSubgroupSettings( String language, String programInfo, boolean improveCompatibility) { BluetoothLeAudioContentMetadata metadata = new BluetoothLeAudioContentMetadata.Builder() .setLanguage(language) .setProgramInfo(programInfo) .build(); // Current broadcast framework only support one subgroup, thus we still maintain the latest // metadata to keep legacy UI working. return new BluetoothLeBroadcastSubgroupSettings.Builder() .setPreferredQuality( improveCompatibility ? BluetoothLeBroadcastSubgroupSettings.QUALITY_STANDARD : BluetoothLeBroadcastSubgroupSettings.QUALITY_HIGH) .setContentMetadata(metadata) .build(); } private final class ProfileListener implements BluetoothProfile.ServiceListener { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { Loading @@ -118,6 +406,19 @@ import java.util.Objects; case BluetoothProfile.LE_AUDIO: mLeAudioProfile = (BluetoothLeAudio) proxy; break; case BluetoothProfile.LE_AUDIO_BROADCAST: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mBroadcastProfile = (BluetoothLeBroadcast) proxy; mBroadcastProfile.registerCallback(mHandler::post, mBroadcastCallback); } break; case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mAssistantProfile = (BluetoothLeBroadcastAssistant) proxy; mAssistantProfile.registerCallback( mHandler::post, mBroadcastAssistantCallback); } break; } } } Loading @@ -135,8 +436,106 @@ import java.util.Objects; case BluetoothProfile.LE_AUDIO: mLeAudioProfile = null; break; case BluetoothProfile.LE_AUDIO_BROADCAST: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mBroadcastProfile.unregisterCallback(mBroadcastCallback); mBroadcastProfile = null; mBroadcastId = 0; } break; case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mAssistantProfile.unregisterCallback(mBroadcastAssistantCallback); mAssistantProfile = null; } break; } } } } private final class BroadcastCallback implements BluetoothLeBroadcast.Callback { @Override public void onBroadcastStarted(int reason, int broadcastId) { mBroadcastId = broadcastId; } @Override public void onBroadcastStartFailed(int reason) { // To prevent broadcast accidentally start when metadata change mDevicesToAdd.clear(); } @Override public void onBroadcastStopped(int reason, int broadcastId) { mBroadcastId = 0; } @Override public void onBroadcastStopFailed(int reason) {} @Override public void onPlaybackStarted(int reason, int broadcastId) {} @Override public void onPlaybackStopped(int reason, int broadcastId) {} @Override public void onBroadcastUpdated(int reason, int broadcastId) {} @Override public void onBroadcastUpdateFailed(int reason, int broadcastId) {} @Override public void onBroadcastMetadataChanged( int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) { BluetoothProfileMonitor.this.addSourceToDevices(mDevicesToAdd, metadata); mDevicesToAdd.clear(); } } private static final class BroadcastAssistantCallback implements BluetoothLeBroadcastAssistant.Callback { @Override public void onSearchStarted(int reason) {} @Override public void onSearchStartFailed(int reason) {} @Override public void onSearchStopped(int reason) {} @Override public void onSearchStopFailed(int reason) {} @Override public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {} @Override public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override public void onSourceAddFailed( @NonNull BluetoothDevice sink, @NonNull BluetoothLeBroadcastMetadata source, int reason) {} @Override public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override public void onReceiveStateChanged( @NonNull BluetoothDevice sink, int sourceId, @NonNull BluetoothLeBroadcastReceiveState state) {} } } services/tests/media/mediarouterservicetest/AndroidManifest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package="com.android.server.media.tests"> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" /> <application android:testOnly="true" android:debuggable="true"> Loading Loading
services/core/java/com/android/server/media/AudioManagerRouteController.java +5 −1 Original line number Diff line number Diff line Loading @@ -177,7 +177,11 @@ import java.util.concurrent.CopyOnWriteArrayList; mBluetoothRouteController = new BluetoothDeviceRoutesManager( mContext, mHandler, btAdapter, this::rebuildAvailableRoutesAndNotify); mContext, mHandler, looper, btAdapter, this::rebuildAvailableRoutesAndNotify); // Just build routes but don't notify. The caller may not expect the listener to be invoked // before this constructor has finished executing. rebuildAvailableRoutes(); Loading
services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java +3 −1 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.media.MediaRoute2Info; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; Loading Loading @@ -94,13 +95,14 @@ import java.util.stream.Collectors; BluetoothDeviceRoutesManager( @NonNull Context context, @NonNull Handler handler, @NonNull Looper looper, @NonNull BluetoothAdapter bluetoothAdapter, @NonNull BluetoothRoutesUpdatedListener listener) { this( context, handler, bluetoothAdapter, new BluetoothProfileMonitor(context, bluetoothAdapter), new BluetoothProfileMonitor(context, looper, bluetoothAdapter), listener); } Loading
services/core/java/com/android/server/media/BluetoothProfileMonitor.java +403 −4 Original line number Diff line number Diff line Loading @@ -23,22 +23,60 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothLeAudioContentMetadata; import android.bluetooth.BluetoothLeBroadcast; import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastSettings; import android.bluetooth.BluetoothLeBroadcastSubgroupSettings; import android.bluetooth.BluetoothProfile; import android.content.ContentResolver; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.media.flags.Flags; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ThreadLocalRandom; /* package */ class BluetoothProfileMonitor { private static final String TAG = BluetoothProfileMonitor.class.getSimpleName(); /* package */ static final long GROUP_ID_NO_GROUP = -1L; private static final String UNDERLINE = "_"; private static final int DEFAULT_CODE_MAX = 9999; private static final int DEFAULT_CODE_MIN = 1000; private static final int MIN_NO_DEVICES_FOR_BROADCAST = 2; private static final String VALID_PASSWORD_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:," + ".<>?/"; private static final int PASSWORD_LENGTH = 16; private static final int BROADCAST_NAME_PREFIX_MAX_LENGTH = 27; private static final String DEFAULT_BROADCAST_NAME_PREFIX = "Broadcast"; private static final String IMPROVE_COMPATIBILITY_HIGH_QUALITY = "1"; @NonNull private final ProfileListener mProfileListener = new ProfileListener(); @NonNull private final BroadcastCallback mBroadcastCallback = new BroadcastCallback(); @NonNull private final Context mContext; @NonNull private final BluetoothAdapter mBluetoothAdapter; private final BroadcastAssistantCallback mBroadcastAssistantCallback = new BroadcastAssistantCallback(); @NonNull private final Context mContext; @NonNull private final Handler mHandler; @NonNull private final BluetoothAdapter mBluetoothAdapter; @Nullable private BluetoothA2dp mA2dpProfile; Loading @@ -47,9 +85,19 @@ import java.util.Objects; @Nullable private BluetoothLeAudio mLeAudioProfile; BluetoothProfileMonitor(@NonNull Context context, @GuardedBy("this") private BluetoothLeBroadcast mBroadcastProfile; private BluetoothLeBroadcastAssistant mAssistantProfile; private final List<BluetoothDevice> mDevicesToAdd = new ArrayList<>(); private int mBroadcastId = 0; BluetoothProfileMonitor( @NonNull Context context, @NonNull Looper looper, @NonNull BluetoothAdapter bluetoothAdapter) { mContext = Objects.requireNonNull(context); mHandler = new Handler(Objects.requireNonNull(looper)); mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter); } Loading @@ -57,6 +105,12 @@ import java.util.Objects; mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEARING_AID); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.LE_AUDIO); if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mBluetoothAdapter.getProfileProxy( mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST); mBluetoothAdapter.getProfileProxy( mContext, mProfileListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); } } /* package */ boolean isProfileSupported(int profile, @NonNull BluetoothDevice device) { Loading Loading @@ -104,6 +158,240 @@ import java.util.Objects; } } /** * Starts broadcast and connect to given bluetooth devices. * * @param devices Bluetooth devices that are going to connect the to broadcast */ public synchronized void startBroadcast(List<BluetoothDevice> devices) { if (devices.size() < MIN_NO_DEVICES_FOR_BROADCAST) { Slog.e( TAG, "Insufficient number of device to start broadcast: need to have at least " + MIN_NO_DEVICES_FOR_BROADCAST + " device(s) to start broadcast, current no. of selected device(s) = " + devices.size()); return; } if (mBroadcastProfile == null) { // BluetoothLeBroadcast is not initialized properly Slog.e(TAG, "Fail to start broadcast, LeBroadcast is null"); return; } if (mBroadcastProfile.getAllBroadcastMetadata().size() >= getMaximumNumberOfBroadcasts()) { Slog.e( TAG, "Fail to start broadcast, current number of broadcasting group: " + mBroadcastProfile.getAllBroadcastMetadata().size() + ", exceeds the maximum allowed: " + getMaximumNumberOfBroadcasts()); return; } // Store the broadcast name so that program info and broadcast setting can use the same // value. String broadcastName = getBroadcastName(); // Current broadcast framework only support one subgroup BluetoothLeBroadcastSubgroupSettings subgroupSettings = buildBroadcastSubgroupSettings( /* language= */ null, getProgramInfo(broadcastName), isImproveQualityFlagEnabled()); BluetoothLeBroadcastSettings settings = buildBroadcastSettings( /* isPublic= */ true, // TODO(b/421062071): Set to false after framework // fix. broadcastName, getBroadcastCode(), List.of(subgroupSettings)); mDevicesToAdd.clear(); mDevicesToAdd.addAll(devices); mBroadcastProfile.startBroadcast(settings); } /** Stops the broadcast. */ public synchronized void stopBroadcast() { if (mBroadcastProfile != null) { mBroadcastProfile.stopBroadcast(mBroadcastId); } else { Slog.e(TAG, "Fail to stop broadcast, LeBroadcast is null"); } } /** * Obtains selected bluetooth devices from broadcast assistant that are broadcasting. * * @return list of selected {@link BluetoothDevice} */ public List<BluetoothDevice> getDevicesWithBroadcastSource() { if (mAssistantProfile == null) { return List.of(); } return mAssistantProfile.getConnectedDevices().stream() .filter( device -> mAssistantProfile.getAllSources(device).stream() .anyMatch( source -> source.getBroadcastId() == mBroadcastId)) .toList(); } /** * Gets the maximum number of Broadcast Isochronous Group supported on this device from * broadcast profile * * @return value of the maximum number of Broadcast Isochronous Group supported on this device * from broadcast profile */ public int getMaximumNumberOfBroadcasts() { return mBroadcastProfile.getMaximumNumberOfBroadcasts(); } /** * Perform add device as broadcast source to {@link BluetoothLeBroadcastAssistant}. Devices will * then receive audio broadcast * * @param deviceList - List of {@link BluetoothDevice} for broadcast * @param metadata - broadcast metadata that obtained from broadcast object */ private void addSourceToDevices( List<BluetoothDevice> deviceList, BluetoothLeBroadcastMetadata metadata) { if (mAssistantProfile == null) { Slog.d(TAG, "BroadcastAssistant is null"); return; } if (metadata == null) { Slog.d(TAG, "BroadcastMetadata is null"); return; } for (BluetoothDevice device : deviceList) { mAssistantProfile.addSource(device, metadata, /* isGroupOp= */ false); } } @Nullable private byte[] getBroadcastCode() { ContentResolver contentResolver = mContext.getContentResolver(); String prefBroadcastCode = Settings.Secure.getStringForUser( contentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE, contentResolver.getUserId()); byte[] broadcastCode = (prefBroadcastCode == null) ? generateRandomPassword().getBytes(StandardCharsets.UTF_8) : prefBroadcastCode.getBytes(StandardCharsets.UTF_8); return (broadcastCode != null && broadcastCode.length > 0) ? broadcastCode : null; } @NonNull private String getBroadcastName() { ContentResolver contentResolver = mContext.getContentResolver(); String settingBroadcastName = Settings.Secure.getStringForUser( contentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME, contentResolver.getUserId()); if (settingBroadcastName == null || settingBroadcastName.isEmpty()) { int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX); String name = BluetoothAdapter.getDefaultAdapter().getName(); if (name == null || name.isEmpty()) { name = DEFAULT_BROADCAST_NAME_PREFIX; } return (name.length() < BROADCAST_NAME_PREFIX_MAX_LENGTH ? name : name.substring(0, BROADCAST_NAME_PREFIX_MAX_LENGTH)) + UNDERLINE + postfix; } return settingBroadcastName; } @NonNull private String getProgramInfo(@NonNull String defaultProgramInfo) { ContentResolver contentResolver = mContext.getContentResolver(); String programInfo = Settings.Secure.getStringForUser( contentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, contentResolver.getUserId()); if (programInfo == null || programInfo.isEmpty()) { return defaultProgramInfo; } return programInfo; } private static String generateRandomPassword() { SecureRandom random = new SecureRandom(); StringBuilder stringBuilder = new StringBuilder(PASSWORD_LENGTH); for (int i = 0; i < PASSWORD_LENGTH; i++) { int randomIndex = random.nextInt(VALID_PASSWORD_CHARACTERS.length()); stringBuilder.append(VALID_PASSWORD_CHARACTERS.charAt(randomIndex)); } return stringBuilder.toString(); } private boolean isImproveQualityFlagEnabled() { ContentResolver contentResolver = mContext.getContentResolver(); // BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY takes only "1" and "0" in string only. Check // android.provider.settings.validators.SecureSettingsValidators for mode details. return IMPROVE_COMPATIBILITY_HIGH_QUALITY.equals( Settings.Secure.getStringForUser( contentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY, contentResolver.getUserId())); } private BluetoothLeBroadcastSettings buildBroadcastSettings( boolean isPublic, String broadcastName, byte[] broadcastCode, List<BluetoothLeBroadcastSubgroupSettings> subgroupSettingsList) { BluetoothLeBroadcastSettings.Builder builder = new BluetoothLeBroadcastSettings.Builder() .setPublicBroadcast(isPublic) .setBroadcastName(broadcastName) .setBroadcastCode(broadcastCode); for (BluetoothLeBroadcastSubgroupSettings subgroupSettings : subgroupSettingsList) { builder.addSubgroupSettings(subgroupSettings); } return builder.build(); } private BluetoothLeBroadcastSubgroupSettings buildBroadcastSubgroupSettings( String language, String programInfo, boolean improveCompatibility) { BluetoothLeAudioContentMetadata metadata = new BluetoothLeAudioContentMetadata.Builder() .setLanguage(language) .setProgramInfo(programInfo) .build(); // Current broadcast framework only support one subgroup, thus we still maintain the latest // metadata to keep legacy UI working. return new BluetoothLeBroadcastSubgroupSettings.Builder() .setPreferredQuality( improveCompatibility ? BluetoothLeBroadcastSubgroupSettings.QUALITY_STANDARD : BluetoothLeBroadcastSubgroupSettings.QUALITY_HIGH) .setContentMetadata(metadata) .build(); } private final class ProfileListener implements BluetoothProfile.ServiceListener { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { Loading @@ -118,6 +406,19 @@ import java.util.Objects; case BluetoothProfile.LE_AUDIO: mLeAudioProfile = (BluetoothLeAudio) proxy; break; case BluetoothProfile.LE_AUDIO_BROADCAST: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mBroadcastProfile = (BluetoothLeBroadcast) proxy; mBroadcastProfile.registerCallback(mHandler::post, mBroadcastCallback); } break; case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mAssistantProfile = (BluetoothLeBroadcastAssistant) proxy; mAssistantProfile.registerCallback( mHandler::post, mBroadcastAssistantCallback); } break; } } } Loading @@ -135,8 +436,106 @@ import java.util.Objects; case BluetoothProfile.LE_AUDIO: mLeAudioProfile = null; break; case BluetoothProfile.LE_AUDIO_BROADCAST: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mBroadcastProfile.unregisterCallback(mBroadcastCallback); mBroadcastProfile = null; mBroadcastId = 0; } break; case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT: if (Flags.enableOutputSwitcherPersonalAudioSharing()) { mAssistantProfile.unregisterCallback(mBroadcastAssistantCallback); mAssistantProfile = null; } break; } } } } private final class BroadcastCallback implements BluetoothLeBroadcast.Callback { @Override public void onBroadcastStarted(int reason, int broadcastId) { mBroadcastId = broadcastId; } @Override public void onBroadcastStartFailed(int reason) { // To prevent broadcast accidentally start when metadata change mDevicesToAdd.clear(); } @Override public void onBroadcastStopped(int reason, int broadcastId) { mBroadcastId = 0; } @Override public void onBroadcastStopFailed(int reason) {} @Override public void onPlaybackStarted(int reason, int broadcastId) {} @Override public void onPlaybackStopped(int reason, int broadcastId) {} @Override public void onBroadcastUpdated(int reason, int broadcastId) {} @Override public void onBroadcastUpdateFailed(int reason, int broadcastId) {} @Override public void onBroadcastMetadataChanged( int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) { BluetoothProfileMonitor.this.addSourceToDevices(mDevicesToAdd, metadata); mDevicesToAdd.clear(); } } private static final class BroadcastAssistantCallback implements BluetoothLeBroadcastAssistant.Callback { @Override public void onSearchStarted(int reason) {} @Override public void onSearchStartFailed(int reason) {} @Override public void onSearchStopped(int reason) {} @Override public void onSearchStopFailed(int reason) {} @Override public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {} @Override public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override public void onSourceAddFailed( @NonNull BluetoothDevice sink, @NonNull BluetoothLeBroadcastMetadata source, int reason) {} @Override public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId, int reason) {} @Override public void onReceiveStateChanged( @NonNull BluetoothDevice sink, int sourceId, @NonNull BluetoothLeBroadcastReceiveState state) {} } }
services/tests/media/mediarouterservicetest/AndroidManifest.xml +1 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package="com.android.server.media.tests"> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" /> <application android:testOnly="true" android:debuggable="true"> Loading