1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.connecteddevice.audiosharing.audiostreams; 18 19 import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID; 20 import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE; 21 import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES; 22 23 import static java.util.Collections.emptyList; 24 25 import android.bluetooth.BluetoothDevice; 26 import android.bluetooth.BluetoothLeAudioContentMetadata; 27 import android.bluetooth.BluetoothLeBroadcastMetadata; 28 import android.bluetooth.BluetoothLeBroadcastReceiveState; 29 import android.bluetooth.BluetoothProfile; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.util.Log; 33 34 import androidx.annotation.VisibleForTesting; 35 36 import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils; 37 import com.android.settingslib.bluetooth.BluetoothUtils; 38 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 39 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 40 import com.android.settingslib.bluetooth.LocalBluetoothManager; 41 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 42 import com.android.settingslib.utils.ThreadUtils; 43 44 import com.google.common.base.Strings; 45 46 import java.util.ArrayList; 47 import java.util.List; 48 import java.util.Optional; 49 import java.util.stream.Stream; 50 51 import javax.annotation.Nullable; 52 53 /** 54 * A helper class that adds, removes and retrieves LE broadcast sources for all active sink devices. 55 */ 56 public class AudioStreamsHelper { 57 58 private static final String TAG = "AudioStreamsHelper"; 59 private static final boolean DEBUG = BluetoothUtils.D; 60 61 private final @Nullable LocalBluetoothManager mBluetoothManager; 62 private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; 63 AudioStreamsHelper(@ullable LocalBluetoothManager bluetoothManager)64 AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) { 65 mBluetoothManager = bluetoothManager; 66 mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager); 67 } 68 69 /** 70 * Adds the specified LE broadcast source to all active sinks. 71 * 72 * @param source The LE broadcast metadata representing the audio source. 73 */ addSource(BluetoothLeBroadcastMetadata source)74 void addSource(BluetoothLeBroadcastMetadata source) { 75 if (mLeBroadcastAssistant == null) { 76 Log.w(TAG, "addSource(): LeBroadcastAssistant is null!"); 77 return; 78 } 79 var unused = 80 ThreadUtils.postOnBackgroundThread( 81 () -> { 82 for (var sink : 83 getConnectedBluetoothDevices( 84 mBluetoothManager, /* inSharingOnly= */ false)) { 85 if (DEBUG) { 86 Log.d( 87 TAG, 88 "addSource(): join broadcast broadcastId" 89 + " : " 90 + source.getBroadcastId() 91 + " sink : " 92 + sink.getAddress()); 93 } 94 mLeBroadcastAssistant.addSource(sink, source, false); 95 } 96 }); 97 } 98 99 /** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */ removeSource(int broadcastId)100 void removeSource(int broadcastId) { 101 if (mLeBroadcastAssistant == null) { 102 Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!"); 103 return; 104 } 105 var unused = 106 ThreadUtils.postOnBackgroundThread( 107 () -> { 108 for (var sink : 109 getConnectedBluetoothDevices( 110 mBluetoothManager, /* inSharingOnly= */ true)) { 111 if (DEBUG) { 112 Log.d( 113 TAG, 114 "removeSource(): remove all sources with broadcast id :" 115 + broadcastId 116 + " from sink : " 117 + sink.getAddress()); 118 } 119 mLeBroadcastAssistant.getAllSources(sink).stream() 120 .filter(state -> state.getBroadcastId() == broadcastId) 121 .forEach( 122 state -> 123 mLeBroadcastAssistant.removeSource( 124 sink, state.getSourceId())); 125 } 126 }); 127 } 128 129 /** Retrieves a list of all LE broadcast receive states from active sinks. */ 130 @VisibleForTesting getAllConnectedSources()131 public List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() { 132 if (mLeBroadcastAssistant == null) { 133 Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!"); 134 return emptyList(); 135 } 136 return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream() 137 .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream()) 138 .filter(AudioStreamsHelper::isConnected) 139 .toList(); 140 } 141 142 /** Retrieves LocalBluetoothLeBroadcastAssistant. */ 143 @VisibleForTesting 144 @Nullable getLeBroadcastAssistant()145 public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() { 146 return mLeBroadcastAssistant; 147 } 148 149 /** Checks the connectivity status based on the provided broadcast receive state. */ isConnected(BluetoothLeBroadcastReceiveState state)150 public static boolean isConnected(BluetoothLeBroadcastReceiveState state) { 151 return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0); 152 } 153 isBadCode(BluetoothLeBroadcastReceiveState state)154 static boolean isBadCode(BluetoothLeBroadcastReceiveState state) { 155 return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED 156 && state.getBigEncryptionState() 157 == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE; 158 } 159 160 /** 161 * Returns a {@code CachedBluetoothDevice} that is either connected to a broadcast source or is 162 * a connected LE device. 163 */ getCachedBluetoothDeviceInSharingOrLeConnected( @ndroidx.annotation.Nullable LocalBluetoothManager manager)164 public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected( 165 @androidx.annotation.Nullable LocalBluetoothManager manager) { 166 if (manager == null) { 167 Log.w( 168 TAG, 169 "getCachedBluetoothDeviceInSharingOrLeConnected(): LocalBluetoothManager is" 170 + " null!"); 171 return Optional.empty(); 172 } 173 var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager); 174 var leadDevices = 175 AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false); 176 if (leadDevices.isEmpty()) { 177 Log.w(TAG, "getCachedBluetoothDeviceInSharingOrLeConnected(): No lead device!"); 178 return Optional.empty(); 179 } 180 var deviceHasSource = 181 leadDevices.stream() 182 .filter(device -> hasConnectedBroadcastSource(device, manager)) 183 .findFirst(); 184 if (deviceHasSource.isPresent()) { 185 Log.d( 186 TAG, 187 "getCachedBluetoothDeviceInSharingOrLeConnected(): Device has connected source" 188 + " found: " 189 + deviceHasSource.get().getAddress()); 190 return deviceHasSource; 191 } 192 Log.d( 193 TAG, 194 "getCachedBluetoothDeviceInSharingOrLeConnected(): Device connected found: " 195 + leadDevices.get(0).getAddress()); 196 return Optional.of(leadDevices.get(0)); 197 } 198 199 /** Returns a {@code CachedBluetoothDevice} that has a connected broadcast source. */ getCachedBluetoothDeviceInSharing( @ndroidx.annotation.Nullable LocalBluetoothManager manager)200 static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharing( 201 @androidx.annotation.Nullable LocalBluetoothManager manager) { 202 if (manager == null) { 203 Log.w(TAG, "getCachedBluetoothDeviceInSharing(): LocalBluetoothManager is null!"); 204 return Optional.empty(); 205 } 206 var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager); 207 var leadDevices = 208 AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false); 209 if (leadDevices.isEmpty()) { 210 Log.w(TAG, "getCachedBluetoothDeviceInSharing(): No lead device!"); 211 return Optional.empty(); 212 } 213 return leadDevices.stream() 214 .filter(device -> hasConnectedBroadcastSource(device, manager)) 215 .findFirst(); 216 } 217 218 /** 219 * Check if {@link CachedBluetoothDevice} has connected to a broadcast source. 220 * 221 * @param cachedDevice The cached bluetooth device to check. 222 * @param localBtManager The BT manager to provide BT functions. 223 * @return Whether the device has connected to a broadcast source. 224 */ hasConnectedBroadcastSource( CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager)225 private static boolean hasConnectedBroadcastSource( 226 CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) { 227 if (localBtManager == null) { 228 Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null"); 229 return false; 230 } 231 LocalBluetoothLeBroadcastAssistant assistant = 232 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 233 if (assistant == null) { 234 Log.d(TAG, "Skip check hasConnectedBroadcastSource due to assistant profile is null"); 235 return false; 236 } 237 List<BluetoothLeBroadcastReceiveState> sourceList = 238 assistant.getAllSources(cachedDevice.getDevice()); 239 if (!sourceList.isEmpty() 240 && sourceList.stream().anyMatch(AudioStreamsHelper::isConnected)) { 241 Log.d( 242 TAG, 243 "Lead device has connected broadcast source, device = " 244 + cachedDevice.getDevice().getAnonymizedAddress()); 245 return true; 246 } 247 // Return true if member device is in broadcast. 248 for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) { 249 List<BluetoothLeBroadcastReceiveState> list = 250 assistant.getAllSources(device.getDevice()); 251 if (!list.isEmpty() && list.stream().anyMatch(AudioStreamsHelper::isConnected)) { 252 Log.d( 253 TAG, 254 "Member device has connected broadcast source, device = " 255 + device.getDevice().getAnonymizedAddress()); 256 return true; 257 } 258 } 259 return false; 260 } 261 262 /** 263 * Retrieves a list of connected Bluetooth devices that belongs to one {@link 264 * CachedBluetoothDevice} that's either connected to a broadcast source or is a connected LE 265 * audio device. 266 */ getConnectedBluetoothDevices( @ullable LocalBluetoothManager manager, boolean inSharingOnly)267 static List<BluetoothDevice> getConnectedBluetoothDevices( 268 @Nullable LocalBluetoothManager manager, boolean inSharingOnly) { 269 if (manager == null) { 270 Log.w(TAG, "getConnectedBluetoothDevices(): LocalBluetoothManager is null!"); 271 return emptyList(); 272 } 273 var leBroadcastAssistant = getLeBroadcastAssistant(manager); 274 if (leBroadcastAssistant == null) { 275 Log.w(TAG, "getConnectedBluetoothDevices(): LeBroadcastAssistant is null!"); 276 return emptyList(); 277 } 278 List<BluetoothDevice> connectedDevices = 279 leBroadcastAssistant.getDevicesMatchingConnectionStates( 280 new int[] {BluetoothProfile.STATE_CONNECTED}); 281 Optional<CachedBluetoothDevice> cachedBluetoothDevice = 282 inSharingOnly 283 ? getCachedBluetoothDeviceInSharing(manager) 284 : getCachedBluetoothDeviceInSharingOrLeConnected(manager); 285 List<BluetoothDevice> bluetoothDevices = 286 cachedBluetoothDevice 287 .map( 288 c -> 289 Stream.concat( 290 Stream.of(c.getDevice()), 291 c.getMemberDevice().stream() 292 .map( 293 CachedBluetoothDevice 294 ::getDevice)) 295 .filter(connectedDevices::contains) 296 .toList()) 297 .orElse(emptyList()); 298 Log.d(TAG, "getConnectedBluetoothDevices() devices: " + bluetoothDevices); 299 return bluetoothDevices; 300 } 301 getLeBroadcastAssistant( @ullable LocalBluetoothManager manager)302 private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant( 303 @Nullable LocalBluetoothManager manager) { 304 if (manager == null) { 305 Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!"); 306 return null; 307 } 308 309 LocalBluetoothProfileManager profileManager = manager.getProfileManager(); 310 if (profileManager == null) { 311 Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!"); 312 return null; 313 } 314 315 return profileManager.getLeAudioBroadcastAssistantProfile(); 316 } 317 getBroadcastName(BluetoothLeBroadcastMetadata source)318 static String getBroadcastName(BluetoothLeBroadcastMetadata source) { 319 String broadcastName = source.getBroadcastName(); 320 if (broadcastName != null && !broadcastName.isEmpty()) { 321 return broadcastName; 322 } 323 return source.getSubgroups().stream() 324 .map(subgroup -> subgroup.getContentMetadata().getProgramInfo()) 325 .filter(programInfo -> !Strings.isNullOrEmpty(programInfo)) 326 .findFirst() 327 .orElse("Broadcast Id: " + source.getBroadcastId()); 328 } 329 getBroadcastName(BluetoothLeBroadcastReceiveState state)330 static String getBroadcastName(BluetoothLeBroadcastReceiveState state) { 331 return state.getSubgroupMetadata().stream() 332 .map(BluetoothLeAudioContentMetadata::getProgramInfo) 333 .filter(i -> !Strings.isNullOrEmpty(i)) 334 .findFirst() 335 .orElse("Broadcast Id: " + state.getBroadcastId()); 336 } 337 startMediaService(Context context, int audioStreamBroadcastId, String title)338 void startMediaService(Context context, int audioStreamBroadcastId, String title) { 339 List<BluetoothDevice> devices = 340 getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true); 341 if (devices.isEmpty()) { 342 return; 343 } 344 var intent = new Intent(context, AudioStreamMediaService.class); 345 intent.putExtra(BROADCAST_ID, audioStreamBroadcastId); 346 intent.putExtra(BROADCAST_TITLE, title); 347 intent.putParcelableArrayListExtra(DEVICES, new ArrayList<>(devices)); 348 context.startService(intent); 349 } 350 } 351