/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settingslib.bluetooth; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; 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.BluetoothLeBroadcastSubgroup; import android.bluetooth.BluetoothLeBroadcastSubgroupSettings; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile.ServiceListener; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.UserManager; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.android.settingslib.R; import com.google.common.collect.ImmutableList; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; /** * LocalBluetoothLeBroadcast provides an interface between the Settings app and the functionality of * the local {@link BluetoothLeBroadcast}. Use the {@link BluetoothLeBroadcast.Callback} to get the * result callback. */ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { public static final String ACTION_LE_AUDIO_SHARING_STATE_CHANGE = "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STATE_CHANGE"; public static final String EXTRA_LE_AUDIO_SHARING_STATE = "BLUETOOTH_LE_AUDIO_SHARING_STATE"; public static final String EXTRA_BLUETOOTH_DEVICE = "BLUETOOTH_DEVICE"; public static final int BROADCAST_STATE_UNKNOWN = 0; public static final int BROADCAST_STATE_ON = 1; public static final int BROADCAST_STATE_OFF = 2; @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = {"BROADCAST_STATE_"}, value = {BROADCAST_STATE_UNKNOWN, BROADCAST_STATE_ON, BROADCAST_STATE_OFF}) public @interface BroadcastState {} private static final String SETTINGS_PKG = "com.android.settings"; private static final String TAG = "LocalBluetoothLeBroadcast"; private static final boolean DEBUG = BluetoothUtils.D; static final String NAME = "LE_AUDIO_BROADCAST"; private static final String UNDERLINE = "_"; private static final int DEFAULT_CODE_MAX = 9999; private static final int DEFAULT_CODE_MIN = 1000; // Order of this profile in device profiles list private static final int ORDINAL = 1; private static final int UNKNOWN_VALUE_PLACEHOLDER = -1; private static final Uri[] SETTINGS_URIS = new Uri[] { Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME), Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO), Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE), Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME), Settings.Secure.getUriFor( Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY), }; private final Context mContext; private final CachedBluetoothDeviceManager mDeviceManager; private BluetoothLeBroadcast mServiceBroadcast; private BluetoothLeBroadcastAssistant mServiceBroadcastAssistant; private BluetoothLeAudioContentMetadata mBluetoothLeAudioContentMetadata; private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata; private BluetoothLeAudioContentMetadata.Builder mBuilder; private int mBroadcastId = UNKNOWN_VALUE_PLACEHOLDER; private String mAppSourceName = ""; private String mNewAppSourceName = ""; private boolean mIsBroadcastProfileReady = false; private boolean mIsBroadcastAssistantProfileReady = false; private boolean mImproveCompatibility = false; private String mProgramInfo; private String mBroadcastName; private byte[] mBroadcastCode; private Executor mExecutor; private ContentResolver mContentResolver; private ContentObserver mSettingsObserver; // Cached broadcast callbacks being register before service is connected. private Map mCachedBroadcastCallbackExecutorMap = new ConcurrentHashMap<>(); private final ServiceListener mServiceListener = new ServiceListener() { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { if (DEBUG) { Log.d(TAG, "Bluetooth service connected: " + profile); } if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST) && !mIsBroadcastProfileReady) { mServiceBroadcast = (BluetoothLeBroadcast) proxy; mIsBroadcastProfileReady = true; registerServiceCallBack(mExecutor, mBroadcastCallback); List metadata = getAllBroadcastMetadata(); if (!metadata.isEmpty()) { updateBroadcastInfoFromBroadcastMetadata(metadata.get(0)); } registerContentObserver(); if (DEBUG) { Log.d( TAG, "onServiceConnected: register " + "mCachedBroadcastCallbackExecutorMap = " + mCachedBroadcastCallbackExecutorMap); } mCachedBroadcastCallbackExecutorMap.forEach( (callback, executor) -> registerServiceCallBack(executor, callback)); } else if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) && !mIsBroadcastAssistantProfileReady) { mIsBroadcastAssistantProfileReady = true; mServiceBroadcastAssistant = (BluetoothLeBroadcastAssistant) proxy; registerBroadcastAssistantCallback(mExecutor, mBroadcastAssistantCallback); } } @Override public void onServiceDisconnected(int profile) { if (DEBUG) { Log.d(TAG, "Bluetooth service disconnected: " + profile); } if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST) && mIsBroadcastProfileReady) { mIsBroadcastProfileReady = false; notifyBroadcastStateChange(BROADCAST_STATE_OFF); unregisterServiceCallBack(mBroadcastCallback); mCachedBroadcastCallbackExecutorMap.clear(); } if ((profile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) && mIsBroadcastAssistantProfileReady) { mIsBroadcastAssistantProfileReady = false; unregisterBroadcastAssistantCallback(mBroadcastAssistantCallback); } if (!mIsBroadcastAssistantProfileReady && !mIsBroadcastProfileReady) { unregisterContentObserver(); } } }; private final BluetoothLeBroadcast.Callback mBroadcastCallback = new BluetoothLeBroadcast.Callback() { @Override public void onBroadcastStarted(int reason, int broadcastId) { if (DEBUG) { Log.d( TAG, "onBroadcastStarted(), reason = " + reason + ", broadcastId = " + broadcastId); } setLatestBroadcastId(broadcastId); setAppSourceName(mNewAppSourceName, /* updateContentResolver= */ true); } @Override public void onBroadcastStartFailed(int reason) { if (DEBUG) { Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason); } } @Override public void onBroadcastMetadataChanged( int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) { if (DEBUG) { Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId); } setLatestBluetoothLeBroadcastMetadata(metadata); notifyBroadcastStateChange(BROADCAST_STATE_ON); } @Override public void onBroadcastStopped(int reason, int broadcastId) { if (DEBUG) { Log.d( TAG, "onBroadcastStopped(), reason = " + reason + ", broadcastId = " + broadcastId); } notifyBroadcastStateChange(BROADCAST_STATE_OFF); stopLocalSourceReceivers(); resetCacheInfo(); } @Override public void onBroadcastStopFailed(int reason) { if (DEBUG) { Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason); } } @Override public void onBroadcastUpdated(int reason, int broadcastId) { if (DEBUG) { Log.d( TAG, "onBroadcastUpdated(), reason = " + reason + ", broadcastId = " + broadcastId); } setLatestBroadcastId(broadcastId); setAppSourceName(mNewAppSourceName, /* updateContentResolver= */ true); } @Override public void onBroadcastUpdateFailed(int reason, int broadcastId) { if (DEBUG) { Log.d( TAG, "onBroadcastUpdateFailed(), reason = " + reason + ", broadcastId = " + broadcastId); } } @Override public void onPlaybackStarted(int reason, int broadcastId) {} @Override public void onPlaybackStopped(int reason, int broadcastId) {} }; private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = new BluetoothLeBroadcastAssistant.Callback() { @Override public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) { if (DEBUG) { Log.d( TAG, "onSourceAdded(), sink = " + sink + ", reason = " + reason + ", sourceId = " + sourceId); } } @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 onSourceAddFailed( @NonNull BluetoothDevice sink, @NonNull BluetoothLeBroadcastMetadata source, int reason) { if (DEBUG) { Log.d( TAG, "onSourceAddFailed(), sink = " + sink + ", reason = " + reason + ", source = " + source); } } @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) { if (DEBUG) { Log.d( TAG, "onSourceRemoved(), sink = " + sink + ", reason = " + reason + ", sourceId = " + sourceId); } } @Override public void onSourceRemoveFailed( @NonNull BluetoothDevice sink, int sourceId, int reason) { if (DEBUG) { Log.d( TAG, "onSourceRemoveFailed(), sink = " + sink + ", reason = " + reason + ", sourceId = " + sourceId); } } @Override public void onReceiveStateChanged( @NonNull BluetoothDevice sink, int sourceId, @NonNull BluetoothLeBroadcastReceiveState state) { if (DEBUG) { Log.d( TAG, "onReceiveStateChanged(), sink = " + sink + ", sourceId = " + sourceId + ", state = " + state); } if (BluetoothUtils.isConnected(state)) { updateFallbackActiveDeviceIfNeeded(); } } }; private class BroadcastSettingsObserver extends ContentObserver { BroadcastSettingsObserver(Handler h) { super(h); } @Override public void onChange(boolean selfChange) { Log.d(TAG, "BroadcastSettingsObserver: onChange"); updateBroadcastInfoFromContentProvider(); } } LocalBluetoothLeBroadcast(Context context, CachedBluetoothDeviceManager deviceManager) { mContext = context; mDeviceManager = deviceManager; mExecutor = Executors.newSingleThreadExecutor(); mBuilder = new BluetoothLeAudioContentMetadata.Builder(); mContentResolver = context.getContentResolver(); Handler handler = new Handler(Looper.getMainLooper()); mSettingsObserver = new BroadcastSettingsObserver(handler); updateBroadcastInfoFromContentProvider(); // Before registering callback, the constructor should finish creating the all of variables. BluetoothAdapter.getDefaultAdapter() .getProfileProxy(context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST); BluetoothAdapter.getDefaultAdapter() .getProfileProxy( context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); } /** * Start the LE Broadcast. If the system started the LE Broadcast, then the system calls the * corresponding callback {@link BluetoothLeBroadcast.Callback}. */ public void startBroadcast(String appSourceName, String language) { mNewAppSourceName = appSourceName; if (mServiceBroadcast == null) { Log.d(TAG, "The BluetoothLeBroadcast is null when starting the broadcast."); return; } String programInfo = getProgramInfo(); if (DEBUG) { Log.d(TAG, "startBroadcast: language = " + language + " ,programInfo = " + programInfo); } buildContentMetadata(language, programInfo); mServiceBroadcast.startBroadcast( mBluetoothLeAudioContentMetadata, (mBroadcastCode != null && mBroadcastCode.length > 0) ? mBroadcastCode : null); } /** * Start the private Broadcast for personal audio sharing or qr code sharing. * *

The broadcast will use random string for both broadcast name and subgroup program info; * The broadcast will use random string for broadcast code; The broadcast will only have one * subgroup due to system limitation; The subgroup language will be null. * *

If the system started the LE Broadcast, then the system calls the corresponding callback * {@link BluetoothLeBroadcast.Callback}. */ public void startPrivateBroadcast() { mNewAppSourceName = "Sharing audio"; if (mServiceBroadcast == null) { Log.d(TAG, "The BluetoothLeBroadcast is null when starting the private broadcast."); return; } if (mServiceBroadcast.getAllBroadcastMetadata().size() >= mServiceBroadcast.getMaximumNumberOfBroadcasts()) { Log.d(TAG, "Skip starting the broadcast due to number limit."); return; } String broadcastName = getBroadcastName(); String programInfo = getProgramInfo(); boolean improveCompatibility = getImproveCompatibility(); if (DEBUG) { Log.d( TAG, "startBroadcast: language = null , programInfo = " + programInfo + ", broadcastName = " + broadcastName + ", improveCompatibility = " + improveCompatibility); } // Current broadcast framework only support one subgroup BluetoothLeBroadcastSubgroupSettings subgroupSettings = buildBroadcastSubgroupSettings( /* language= */ null, programInfo, improveCompatibility); BluetoothLeBroadcastSettings settings = buildBroadcastSettings( true, // TODO: set to false after framework fix TextUtils.isEmpty(broadcastName) ? null : broadcastName, (mBroadcastCode != null && mBroadcastCode.length > 0) ? mBroadcastCode : null, ImmutableList.of(subgroupSettings)); mServiceBroadcast.startBroadcast(settings); } /** Checks if the broadcast is playing. */ public boolean isPlaying(int broadcastId) { if (mServiceBroadcast == null) { Log.d(TAG, "check isPlaying failed, the BluetoothLeBroadcast is null."); return false; } return mServiceBroadcast.isPlaying(broadcastId); } private BluetoothLeBroadcastSettings buildBroadcastSettings( boolean isPublic, @Nullable String broadcastName, @Nullable byte[] broadcastCode, List 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( @Nullable String language, @Nullable 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. mBluetoothLeAudioContentMetadata = metadata; return new BluetoothLeBroadcastSubgroupSettings.Builder() .setPreferredQuality( improveCompatibility ? BluetoothLeBroadcastSubgroupSettings.QUALITY_STANDARD : BluetoothLeBroadcastSubgroupSettings.QUALITY_HIGH) .setContentMetadata(mBluetoothLeAudioContentMetadata) .build(); } public String getProgramInfo() { return mProgramInfo; } public void setProgramInfo(String programInfo) { setProgramInfo(programInfo, /* updateContentResolver= */ true); } private void setProgramInfo(String programInfo, boolean updateContentResolver) { if (TextUtils.isEmpty(programInfo)) { Log.d(TAG, "setProgramInfo: programInfo is null or empty"); return; } if (mProgramInfo != null && TextUtils.equals(mProgramInfo, programInfo)) { Log.d(TAG, "setProgramInfo: programInfo is not changed"); return; } Log.d(TAG, "setProgramInfo: " + programInfo); mProgramInfo = programInfo; if (updateContentResolver) { if (mContentResolver == null) { Log.d(TAG, "mContentResolver is null"); return; } Settings.Secure.putString( mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, programInfo); } } public String getBroadcastName() { return mBroadcastName; } /** Set broadcast name. */ public void setBroadcastName(String broadcastName) { setBroadcastName(broadcastName, /* updateContentResolver= */ true); } private void setBroadcastName(String broadcastName, boolean updateContentResolver) { if (TextUtils.isEmpty(broadcastName)) { Log.d(TAG, "setBroadcastName: broadcastName is null or empty"); return; } if (mBroadcastName != null && TextUtils.equals(mBroadcastName, broadcastName)) { Log.d(TAG, "setBroadcastName: broadcastName is not changed"); return; } Log.d(TAG, "setBroadcastName: " + broadcastName); mBroadcastName = broadcastName; if (updateContentResolver) { if (mContentResolver == null) { Log.d(TAG, "mContentResolver is null"); return; } Settings.Secure.putString( mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME, broadcastName); } } public byte[] getBroadcastCode() { return mBroadcastCode; } public void setBroadcastCode(byte[] broadcastCode) { setBroadcastCode(broadcastCode, /* updateContentResolver= */ true); } private void setBroadcastCode(byte[] broadcastCode, boolean updateContentResolver) { if (broadcastCode == null) { Log.d(TAG, "setBroadcastCode: broadcastCode is null"); return; } if (mBroadcastCode != null && Arrays.equals(broadcastCode, mBroadcastCode)) { Log.d(TAG, "setBroadcastCode: broadcastCode is not changed"); return; } mBroadcastCode = broadcastCode; if (updateContentResolver) { if (mContentResolver == null) { Log.d(TAG, "mContentResolver is null"); return; } Settings.Secure.putString( mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE, new String(broadcastCode, StandardCharsets.UTF_8)); } } /** Get compatibility config for broadcast. */ public boolean getImproveCompatibility() { return mImproveCompatibility; } /** Set compatibility config for broadcast. */ public void setImproveCompatibility(boolean improveCompatibility) { setImproveCompatibility(improveCompatibility, /* updateContentResolver= */ true); } private void setImproveCompatibility( boolean improveCompatibility, boolean updateContentResolver) { if (mImproveCompatibility == improveCompatibility) { Log.d(TAG, "setImproveCompatibility: improveCompatibility is not changed"); return; } mImproveCompatibility = improveCompatibility; if (updateContentResolver) { if (mContentResolver == null) { Log.d(TAG, "mContentResolver is null"); return; } Log.d(TAG, "Set improveCompatibility to: " + improveCompatibility); Settings.Secure.putString( mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY, improveCompatibility ? "1" : "0"); } } private void setLatestBroadcastId(int broadcastId) { Log.d(TAG, "setLatestBroadcastId: mBroadcastId is " + broadcastId); mBroadcastId = broadcastId; } public int getLatestBroadcastId() { return mBroadcastId; } private void setAppSourceName(String appSourceName, boolean updateContentResolver) { if (TextUtils.isEmpty(appSourceName)) { appSourceName = ""; } if (mAppSourceName != null && TextUtils.equals(mAppSourceName, appSourceName)) { Log.d(TAG, "setAppSourceName: appSourceName is not changed"); return; } mAppSourceName = appSourceName; mNewAppSourceName = ""; if (updateContentResolver) { if (mContentResolver == null) { Log.d(TAG, "mContentResolver is null"); return; } Settings.Secure.putString( mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, mAppSourceName); } } public String getAppSourceName() { return mAppSourceName; } private void setLatestBluetoothLeBroadcastMetadata( BluetoothLeBroadcastMetadata bluetoothLeBroadcastMetadata) { if (bluetoothLeBroadcastMetadata != null && bluetoothLeBroadcastMetadata.getBroadcastId() == mBroadcastId) { mBluetoothLeBroadcastMetadata = bluetoothLeBroadcastMetadata; updateBroadcastInfoFromBroadcastMetadata(bluetoothLeBroadcastMetadata); } } public BluetoothLeBroadcastMetadata getLatestBluetoothLeBroadcastMetadata() { if (mServiceBroadcast == null) { Log.d(TAG, "The BluetoothLeBroadcast is null"); return null; } if (mBluetoothLeBroadcastMetadata == null) { final List metadataList = mServiceBroadcast.getAllBroadcastMetadata(); mBluetoothLeBroadcastMetadata = metadataList.stream() .filter(i -> i.getBroadcastId() == mBroadcastId) .findFirst() .orElse(null); } return mBluetoothLeBroadcastMetadata; } private void updateBroadcastInfoFromContentProvider() { if (mContentResolver == null) { Log.d(TAG, "updateBroadcastInfoFromContentProvider: mContentResolver is null"); return; } String programInfo = Settings.Secure.getString( mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO); if (programInfo == null) { programInfo = getDefaultValueOfProgramInfo(); } setProgramInfo(programInfo, /* updateContentResolver= */ false); String broadcastName = Settings.Secure.getString( mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME); if (broadcastName == null) { broadcastName = getDefaultValueOfBroadcastName(); } setBroadcastName(broadcastName, /* updateContentResolver= */ false); String prefBroadcastCode = Settings.Secure.getString( mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE); byte[] broadcastCode = (prefBroadcastCode == null) ? getDefaultValueOfBroadcastCode() : prefBroadcastCode.getBytes(StandardCharsets.UTF_8); setBroadcastCode(broadcastCode, /* updateContentResolver= */ false); String appSourceName = Settings.Secure.getString( mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME); setAppSourceName(appSourceName, /* updateContentResolver= */ false); String improveCompatibility = Settings.Secure.getString( mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY); setImproveCompatibility( improveCompatibility == null ? false : improveCompatibility.equals("1"), /* updateContentResolver= */ false); } private void updateBroadcastInfoFromBroadcastMetadata( BluetoothLeBroadcastMetadata bluetoothLeBroadcastMetadata) { if (bluetoothLeBroadcastMetadata == null) { Log.d(TAG, "The bluetoothLeBroadcastMetadata is null"); return; } setBroadcastName(bluetoothLeBroadcastMetadata.getBroadcastName()); setBroadcastCode(bluetoothLeBroadcastMetadata.getBroadcastCode()); setLatestBroadcastId(bluetoothLeBroadcastMetadata.getBroadcastId()); List subgroup = bluetoothLeBroadcastMetadata.getSubgroups(); if (subgroup == null || subgroup.size() < 1) { Log.d(TAG, "The subgroup is not valid value"); return; } BluetoothLeAudioContentMetadata contentMetadata = subgroup.get(0).getContentMetadata(); setProgramInfo(contentMetadata.getProgramInfo()); setAppSourceName(getAppSourceName(), /* updateContentResolver= */ true); } /** * Stop the latest LE Broadcast. If the system stopped the LE Broadcast, then the system calls * the corresponding callback {@link BluetoothLeBroadcast.Callback}. */ public void stopLatestBroadcast() { stopBroadcast(mBroadcastId); } /** * Stop the LE Broadcast. If the system stopped the LE Broadcast, then the system calls the * corresponding callback {@link BluetoothLeBroadcast.Callback}. */ public void stopBroadcast(int broadcastId) { if (mServiceBroadcast == null) { Log.d(TAG, "The BluetoothLeBroadcast is null when stopping the broadcast."); return; } if (DEBUG) { Log.d(TAG, "stopBroadcast()"); } mServiceBroadcast.stopBroadcast(broadcastId); } /** * Update the LE Broadcast. If the system stopped the LE Broadcast, then the system calls the * corresponding callback {@link BluetoothLeBroadcast.Callback}. */ public void updateBroadcast(String appSourceName, String language) { if (mServiceBroadcast == null) { Log.d(TAG, "The BluetoothLeBroadcast is null when updating the broadcast."); return; } String programInfo = getProgramInfo(); if (DEBUG) { Log.d( TAG, "updateBroadcast: language = " + language + " ,programInfo = " + programInfo); } mNewAppSourceName = appSourceName; mBluetoothLeAudioContentMetadata = mBuilder.setProgramInfo(programInfo).build(); mServiceBroadcast.updateBroadcast(mBroadcastId, mBluetoothLeAudioContentMetadata); } /** * Update the LE Broadcast by calling {@link BluetoothLeBroadcast#updateBroadcast(int, * BluetoothLeBroadcastSettings)}, currently only updates broadcast name and program info. */ public void updateBroadcast() { if (mServiceBroadcast == null) { Log.d(TAG, "The BluetoothLeBroadcast is null when updating the broadcast."); return; } String programInfo = getProgramInfo(); String broadcastName = getBroadcastName(); mBluetoothLeAudioContentMetadata = mBuilder.setProgramInfo(programInfo).build(); // LeAudioService#updateBroadcast doesn't update broadcastCode, isPublicBroadcast and // preferredQuality, so we leave them unset here. // TODO: maybe setPublicBroadcastMetadata BluetoothLeBroadcastSettings settings = new BluetoothLeBroadcastSettings.Builder() .setBroadcastName(broadcastName) .addSubgroupSettings( new BluetoothLeBroadcastSubgroupSettings.Builder() .setContentMetadata(mBluetoothLeAudioContentMetadata) .build()) .build(); if (DEBUG) { Log.d( TAG, "updateBroadcast: broadcastName = " + broadcastName + " programInfo = " + programInfo); } mServiceBroadcast.updateBroadcast(mBroadcastId, settings); } /** * Register Broadcast Callbacks to track its state and receivers * * @param executor Executor object for callback * @param callback Callback object to be registered */ public void registerServiceCallBack( @NonNull @CallbackExecutor Executor executor, @NonNull BluetoothLeBroadcast.Callback callback) { if (mServiceBroadcast == null) { Log.d(TAG, "registerServiceCallBack failed, the BluetoothLeBroadcast is null."); mCachedBroadcastCallbackExecutorMap.putIfAbsent(callback, executor); return; } try { mServiceBroadcast.registerCallback(executor, callback); } catch (IllegalArgumentException e) { Log.w(TAG, "registerServiceCallBack failed. " + e.getMessage()); } } /** * Register Broadcast Assistant Callbacks to track its state and receivers * * @param executor Executor object for callback * @param callback Callback object to be registered */ private void registerBroadcastAssistantCallback( @NonNull @CallbackExecutor Executor executor, @NonNull BluetoothLeBroadcastAssistant.Callback callback) { if (mServiceBroadcastAssistant == null) { Log.d( TAG, "registerBroadcastAssistantCallback failed, " + "the BluetoothLeBroadcastAssistant is null."); return; } mServiceBroadcastAssistant.registerCallback(executor, callback); } /** * Unregister previously registered Broadcast Callbacks * * @param callback Callback object to be unregistered */ public void unregisterServiceCallBack(@NonNull BluetoothLeBroadcast.Callback callback) { mCachedBroadcastCallbackExecutorMap.remove(callback); if (mServiceBroadcast == null) { Log.d(TAG, "unregisterServiceCallBack failed, the BluetoothLeBroadcast is null."); return; } try { mServiceBroadcast.unregisterCallback(callback); } catch (IllegalArgumentException e) { Log.w(TAG, "unregisterServiceCallBack failed. " + e.getMessage()); } } /** * Unregister previously registered Broadcast Assistant Callbacks * * @param callback Callback object to be unregistered */ private void unregisterBroadcastAssistantCallback( @NonNull BluetoothLeBroadcastAssistant.Callback callback) { if (mServiceBroadcastAssistant == null) { Log.d( TAG, "unregisterBroadcastAssistantCallback, " + "the BluetoothLeBroadcastAssistant is null."); return; } mServiceBroadcastAssistant.unregisterCallback(callback); } private void buildContentMetadata(String language, String programInfo) { mBluetoothLeAudioContentMetadata = mBuilder.setLanguage(language).setProgramInfo(programInfo).build(); } public LocalBluetoothLeBroadcastMetadata getLocalBluetoothLeBroadcastMetaData() { final BluetoothLeBroadcastMetadata metadata = getLatestBluetoothLeBroadcastMetadata(); if (metadata == null) { Log.d(TAG, "The BluetoothLeBroadcastMetadata is null."); return null; } return new LocalBluetoothLeBroadcastMetadata(metadata); } public boolean isProfileReady() { return mIsBroadcastProfileReady; } @Override public int getProfileId() { return BluetoothProfile.LE_AUDIO_BROADCAST; } public boolean accessProfileEnabled() { return false; } public boolean isAutoConnectable() { return true; } /** Not supported since LE Audio Broadcasts do not establish a connection. */ public int getConnectionStatus(BluetoothDevice device) { if (mServiceBroadcast == null) { return BluetoothProfile.STATE_DISCONNECTED; } // LE Audio Broadcasts are not connection-oriented. return mServiceBroadcast.getConnectionState(device); } /** Not supported since LE Audio Broadcasts do not establish a connection. */ public List getConnectedDevices() { if (mServiceBroadcast == null) { return new ArrayList(0); } // LE Audio Broadcasts are not connection-oriented. return mServiceBroadcast.getConnectedDevices(); } /** Get all broadcast metadata. */ public @NonNull List getAllBroadcastMetadata() { if (mServiceBroadcast == null) { Log.d(TAG, "The BluetoothLeBroadcast is null."); return Collections.emptyList(); } return mServiceBroadcast.getAllBroadcastMetadata(); } public boolean isEnabled(BluetoothDevice device) { if (mServiceBroadcast == null) { return false; } return !mServiceBroadcast.getAllBroadcastMetadata().isEmpty(); } /** Service does not provide method to get/set policy. */ public int getConnectionPolicy(BluetoothDevice device) { return CONNECTION_POLICY_FORBIDDEN; } /** * Service does not provide "setEnabled" method. Please use {@link #startBroadcast}, {@link * #stopBroadcast()} or {@link #updateBroadcast(String, String)} */ public boolean setEnabled(BluetoothDevice device, boolean enabled) { return false; } public String toString() { return NAME; } public int getOrdinal() { return ORDINAL; } public int getNameResource(BluetoothDevice device) { return R.string.summary_empty; } public int getSummaryResourceForDevice(BluetoothDevice device) { int state = getConnectionStatus(device); return BluetoothUtils.getConnectionStateSummary(state); } public int getDrawableResource(BluetoothClass btClass) { return 0; } @RequiresApi(Build.VERSION_CODES.S) protected void finalize() { if (DEBUG) { Log.d(TAG, "finalize()"); } if (mServiceBroadcast != null) { try { BluetoothAdapter.getDefaultAdapter() .closeProfileProxy(BluetoothProfile.LE_AUDIO_BROADCAST, mServiceBroadcast); mServiceBroadcast = null; } catch (Throwable t) { Log.w(TAG, "Error cleaning up LeAudio proxy", t); } } } private String getDefaultValueOfBroadcastName() { // set the default value; int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX); return BluetoothAdapter.getDefaultAdapter().getName() + UNDERLINE + postfix; } private String getDefaultValueOfProgramInfo() { // set the default value; int postfix = ThreadLocalRandom.current().nextInt(DEFAULT_CODE_MIN, DEFAULT_CODE_MAX); return BluetoothAdapter.getDefaultAdapter().getName() + UNDERLINE + postfix; } private byte[] getDefaultValueOfBroadcastCode() { // set the default value; return generateRandomPassword().getBytes(StandardCharsets.UTF_8); } private void resetCacheInfo() { if (DEBUG) { Log.d(TAG, "resetCacheInfo:"); } setAppSourceName("", /* updateContentResolver= */ true); mBluetoothLeBroadcastMetadata = null; mBroadcastId = UNKNOWN_VALUE_PLACEHOLDER; } private String generateRandomPassword() { String randomUUID = UUID.randomUUID().toString(); // first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx return randomUUID.substring(0, 8) + randomUUID.substring(9, 13); } private void registerContentObserver() { if (mContentResolver == null) { Log.d(TAG, "mContentResolver is null"); return; } for (Uri uri : SETTINGS_URIS) { mContentResolver.registerContentObserver(uri, false, mSettingsObserver); } } private void unregisterContentObserver() { if (mContentResolver == null) { Log.d(TAG, "mContentResolver is null"); return; } mContentResolver.unregisterContentObserver(mSettingsObserver); } private void stopLocalSourceReceivers() { if (DEBUG) { Log.d(TAG, "stopLocalSourceReceivers()"); } for (BluetoothDevice device : mServiceBroadcastAssistant.getConnectedDevices()) { for (BluetoothLeBroadcastReceiveState receiveState : mServiceBroadcastAssistant.getAllSources(device)) { /* Check if local/last broadcast is the synced one */ int localBroadcastId = getLatestBroadcastId(); if (receiveState.getBroadcastId() != localBroadcastId) continue; mServiceBroadcastAssistant.removeSource(device, receiveState.getSourceId()); } } } /** Update fallback active device if needed. */ public void updateFallbackActiveDeviceIfNeeded() { if (mServiceBroadcast == null) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to broadcast profile is null"); return; } List sources = mServiceBroadcast.getAllBroadcastMetadata(); if (sources.stream() .noneMatch(source -> mServiceBroadcast.isPlaying(source.getBroadcastId()))) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no broadcast ongoing"); return; } if (mServiceBroadcastAssistant == null) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to assistant profile is null"); return; } List connectedDevices = mServiceBroadcastAssistant.getConnectedDevices(); List devicesInSharing = connectedDevices.stream() .filter( bluetoothDevice -> { List sourceList = mServiceBroadcastAssistant.getAllSources( bluetoothDevice); return !sourceList.isEmpty() && sourceList.stream() .anyMatch(BluetoothUtils::isConnected); }) .collect(Collectors.toList()); if (devicesInSharing.isEmpty()) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no sinks in broadcast"); return; } List devices = BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices(); BluetoothDevice targetDevice = null; // Find the earliest connected device in sharing session. int targetDeviceIdx = -1; for (BluetoothDevice device : devicesInSharing) { if (devices.contains(device)) { int idx = devices.indexOf(device); if (idx > targetDeviceIdx) { targetDeviceIdx = idx; targetDevice = device; } } } if (targetDevice == null) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, target is null"); return; } Log.d( TAG, "updateFallbackActiveDeviceIfNeeded, set active device: " + targetDevice.getAnonymizedAddress()); CachedBluetoothDevice targetCachedDevice = mDeviceManager.findDevice(targetDevice); if (targetCachedDevice == null) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find cached bt device"); return; } int fallbackActiveGroupId = getFallbackActiveGroupId(); if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID && getGroupId(targetCachedDevice) == fallbackActiveGroupId) { Log.d( TAG, "Skip updateFallbackActiveDeviceIfNeeded, already is fallback: " + fallbackActiveGroupId); return; } targetCachedDevice.setActive(); } private int getFallbackActiveGroupId() { return Settings.Secure.getInt( mContext.getContentResolver(), "bluetooth_le_broadcast_fallback_active_group_id", BluetoothCsipSetCoordinator.GROUP_ID_INVALID); } private int getGroupId(CachedBluetoothDevice cachedDevice) { int groupId = cachedDevice.getGroupId(); String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress(); if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress); return groupId; } for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) { if (profile instanceof LeAudioProfile) { Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress); return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice()); } } Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress); return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; } private void notifyBroadcastStateChange(@BroadcastState int state) { if (!mContext.getPackageName().equals(SETTINGS_PKG)) { Log.d(TAG, "Skip notifyBroadcastStateChange, not triggered by Settings."); return; } if (isWorkProfile(mContext)) { Log.d(TAG, "Skip notifyBroadcastStateChange, not triggered for work profile."); return; } Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE); intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, state); intent.setPackage(mContext.getPackageName()); Log.e(TAG, "notifyBroadcastStateChange for state = " + state); mContext.sendBroadcast(intent); } private boolean isWorkProfile(Context context) { UserManager userManager = context.getSystemService(UserManager.class); return userManager != null && userManager.isManagedProfile(); } }