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 import static com.android.settingslib.accessibility.AccessibilityUtils.setAccessibilityServiceState; 23 import static com.android.settingslib.bluetooth.BluetoothUtils.isAudioSharingHysteresisModeFixAvailable; 24 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState; 25 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.DECRYPTION_FAILED; 26 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED; 27 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING; 28 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.getLocalSourceState; 29 30 import static java.util.Collections.emptyList; 31 import static java.util.Collections.emptyMap; 32 import static java.util.stream.Collectors.toMap; 33 34 import android.accessibilityservice.AccessibilityServiceInfo; 35 import android.bluetooth.BluetoothDevice; 36 import android.bluetooth.BluetoothLeAudioContentMetadata; 37 import android.bluetooth.BluetoothLeBroadcastMetadata; 38 import android.bluetooth.BluetoothLeBroadcastReceiveState; 39 import android.content.ComponentName; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.content.res.Configuration; 43 import android.util.Log; 44 import android.util.Pair; 45 import android.view.accessibility.AccessibilityManager; 46 47 import androidx.annotation.VisibleForTesting; 48 import androidx.fragment.app.FragmentActivity; 49 50 import com.android.settings.R; 51 import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils; 52 import com.android.settingslib.bluetooth.BluetoothUtils; 53 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 54 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 55 import com.android.settingslib.bluetooth.LocalBluetoothManager; 56 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 57 import com.android.settingslib.utils.ThreadUtils; 58 59 import com.google.android.material.appbar.AppBarLayout; 60 import com.google.common.base.Strings; 61 62 import java.util.ArrayList; 63 import java.util.Collections; 64 import java.util.HashSet; 65 import java.util.List; 66 import java.util.Map; 67 import java.util.Optional; 68 import java.util.Set; 69 import java.util.function.Function; 70 import java.util.stream.Stream; 71 72 import javax.annotation.Nullable; 73 74 /** 75 * A helper class that adds, removes and retrieves LE broadcast sources for all active sink devices. 76 */ 77 public class AudioStreamsHelper { 78 79 private static final String TAG = "AudioStreamsHelper"; 80 private static final boolean DEBUG = BluetoothUtils.D; 81 82 private final @Nullable LocalBluetoothManager mBluetoothManager; 83 private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; 84 AudioStreamsHelper(@ullable LocalBluetoothManager bluetoothManager)85 AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) { 86 mBluetoothManager = bluetoothManager; 87 mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager); 88 } 89 90 /** 91 * Adds the specified LE broadcast source to all active sinks. 92 * 93 * @param source The LE broadcast metadata representing the audio source. 94 */ 95 @VisibleForTesting addSource(BluetoothLeBroadcastMetadata source)96 public void addSource(BluetoothLeBroadcastMetadata source) { 97 if (mLeBroadcastAssistant == null) { 98 Log.w(TAG, "addSource(): LeBroadcastAssistant is null!"); 99 return; 100 } 101 var unused = 102 ThreadUtils.postOnBackgroundThread( 103 () -> { 104 for (var sink : 105 getConnectedBluetoothDevices( 106 mBluetoothManager, /* inSharingOnly= */ false)) { 107 if (DEBUG) { 108 Log.d( 109 TAG, 110 "addSource(): join broadcast broadcastId" 111 + " : " 112 + source.getBroadcastId() 113 + " sink : " 114 + sink.getAddress()); 115 } 116 mLeBroadcastAssistant.addSource(sink, source, false); 117 } 118 }); 119 } 120 121 /** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */ 122 @VisibleForTesting removeSource(int broadcastId)123 public void removeSource(int broadcastId) { 124 if (mLeBroadcastAssistant == null) { 125 Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!"); 126 return; 127 } 128 var unused = 129 ThreadUtils.postOnBackgroundThread( 130 () -> { 131 for (var sink : 132 getConnectedBluetoothDevices( 133 mBluetoothManager, /* inSharingOnly= */ true)) { 134 if (DEBUG) { 135 Log.d( 136 TAG, 137 "removeSource(): remove all sources with broadcast id :" 138 + broadcastId 139 + " from sink : " 140 + sink.getAddress()); 141 } 142 mLeBroadcastAssistant.getAllSources(sink).stream() 143 .filter(state -> state.getBroadcastId() == broadcastId) 144 .forEach( 145 state -> 146 mLeBroadcastAssistant.removeSource( 147 sink, state.getSourceId())); 148 } 149 }); 150 } 151 152 /** 153 * Gets a map of connected broadcast IDs to their corresponding local broadcast source states. 154 * 155 * <p>If multiple sources have the same broadcast ID, the state of the source that is 156 * {@code STREAMING} is preferred. 157 */ getConnectedBroadcastIdAndState( boolean hysteresisModeFixAvailable)158 public Map<Integer, LocalBluetoothLeBroadcastSourceState> getConnectedBroadcastIdAndState( 159 boolean hysteresisModeFixAvailable) { 160 if (mBluetoothManager == null || mLeBroadcastAssistant == null) { 161 Log.w(TAG, 162 "getConnectedBroadcastIdAndState(): BluetoothManager or LeBroadcastAssistant " 163 + "is null!"); 164 return emptyMap(); 165 } 166 return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream() 167 .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream()) 168 .map(state -> new Pair<>(state.getBroadcastId(), getLocalSourceState(state))) 169 .filter(pair -> pair.second == STREAMING 170 || (hysteresisModeFixAvailable && pair.second == PAUSED)) 171 .collect(toMap( 172 p -> p.first, 173 p -> p.second, 174 (existingState, newState) -> existingState == STREAMING ? existingState 175 : newState 176 )); 177 } 178 179 /** Retrieves a list of all LE broadcast receive states keyed by each active device. */ getAllSourcesByDevice()180 public Map<BluetoothDevice, List<BluetoothLeBroadcastReceiveState>> getAllSourcesByDevice() { 181 if (mLeBroadcastAssistant == null) { 182 Log.w(TAG, "getAllSourcesByDevice(): LeBroadcastAssistant is null!"); 183 return emptyMap(); 184 } 185 return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream() 186 .collect(toMap(Function.identity(), mLeBroadcastAssistant::getAllSources)); 187 } 188 189 /** Retrieves LocalBluetoothLeBroadcastAssistant. */ 190 @Nullable getLeBroadcastAssistant()191 public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() { 192 return mLeBroadcastAssistant; 193 } 194 195 /** 196 * Returns a {@code CachedBluetoothDevice} that is either connected to a broadcast source or is 197 * a connected LE device. 198 */ getCachedBluetoothDeviceInSharingOrLeConnected( @ndroidx.annotation.Nullable LocalBluetoothManager manager)199 public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected( 200 @androidx.annotation.Nullable LocalBluetoothManager manager) { 201 if (manager == null) { 202 Log.w( 203 TAG, 204 "getCachedBluetoothDeviceInSharingOrLeConnected(): LocalBluetoothManager is" 205 + " null!"); 206 return Optional.empty(); 207 } 208 var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager); 209 var leadDevices = 210 AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false); 211 if (leadDevices.isEmpty()) { 212 Log.w(TAG, "getCachedBluetoothDeviceInSharingOrLeConnected(): No lead device!"); 213 return Optional.empty(); 214 } 215 var deviceHasSource = 216 leadDevices.stream() 217 .filter(device -> hasBroadcastSource(device, manager)) 218 .findFirst(); 219 if (deviceHasSource.isPresent()) { 220 Log.d( 221 TAG, 222 "getCachedBluetoothDeviceInSharingOrLeConnected(): Device has connected source" 223 + " found: " 224 + deviceHasSource.get().getAddress()); 225 return deviceHasSource; 226 } 227 Log.d( 228 TAG, 229 "getCachedBluetoothDeviceInSharingOrLeConnected(): Device connected found: " 230 + leadDevices.get(0).getAddress()); 231 return Optional.of(leadDevices.get(0)); 232 } 233 234 /** Returns a {@code CachedBluetoothDevice} that has a connected broadcast source. */ getCachedBluetoothDeviceInSharing( @ndroidx.annotation.Nullable LocalBluetoothManager manager)235 static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharing( 236 @androidx.annotation.Nullable LocalBluetoothManager manager) { 237 if (manager == null) { 238 Log.w(TAG, "getCachedBluetoothDeviceInSharing(): LocalBluetoothManager is null!"); 239 return Optional.empty(); 240 } 241 var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager); 242 var leadDevices = 243 AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false); 244 if (leadDevices.isEmpty()) { 245 Log.w(TAG, "getCachedBluetoothDeviceInSharing(): No lead device!"); 246 return Optional.empty(); 247 } 248 return leadDevices.stream() 249 .filter(device -> hasBroadcastSource(device, manager)) 250 .findFirst(); 251 } 252 253 /** 254 * Check if {@link CachedBluetoothDevice} has a broadcast source that is in STREAMING, PAUSED 255 * or DECRYPTION_FAILED state. 256 * 257 * @param cachedDevice The cached bluetooth device to check. 258 * @param localBtManager The BT manager to provide BT functions. 259 * @return Whether the device has a broadcast source. 260 */ hasBroadcastSource( CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager)261 public static boolean hasBroadcastSource( 262 CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) { 263 if (localBtManager == null) { 264 Log.d(TAG, "Skip check hasBroadcastSource due to bt manager is null"); 265 return false; 266 } 267 LocalBluetoothLeBroadcastAssistant assistant = 268 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 269 if (assistant == null) { 270 Log.d(TAG, "Skip check hasBroadcastSource due to assistant profile is null"); 271 return false; 272 } 273 List<BluetoothLeBroadcastReceiveState> sourceList = 274 assistant.getAllSources(cachedDevice.getDevice()); 275 boolean hysteresisModeFixAvailable = isAudioSharingHysteresisModeFixAvailable( 276 localBtManager.getContext()); 277 if (hasReceiveState(sourceList, hysteresisModeFixAvailable)) { 278 Log.d( 279 TAG, 280 "Lead device has broadcast source, device = " 281 + cachedDevice.getDevice().getAnonymizedAddress()); 282 return true; 283 } 284 // Return true if member device is in broadcast. 285 for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) { 286 List<BluetoothLeBroadcastReceiveState> list = 287 assistant.getAllSources(device.getDevice()); 288 if (hasReceiveState(list, hysteresisModeFixAvailable)) { 289 Log.d( 290 TAG, 291 "Member device has broadcast source, device = " 292 + device.getDevice().getAnonymizedAddress()); 293 return true; 294 } 295 } 296 return false; 297 } 298 hasReceiveState(List<BluetoothLeBroadcastReceiveState> states, boolean hysteresisModeFixAvailable)299 private static boolean hasReceiveState(List<BluetoothLeBroadcastReceiveState> states, 300 boolean hysteresisModeFixAvailable) { 301 return states.stream().anyMatch(state -> { 302 var localSourceState = getLocalSourceState(state); 303 if (hysteresisModeFixAvailable) { 304 return localSourceState == STREAMING || localSourceState == DECRYPTION_FAILED 305 || localSourceState == PAUSED; 306 } 307 return localSourceState == STREAMING || localSourceState == DECRYPTION_FAILED; 308 }); 309 } 310 311 /** 312 * Retrieves a list of connected Bluetooth devices that belongs to one {@link 313 * CachedBluetoothDevice} that's either connected to a broadcast source or is a connected LE 314 * audio device. 315 */ getConnectedBluetoothDevices( @ullable LocalBluetoothManager manager, boolean inSharingOnly)316 static List<BluetoothDevice> getConnectedBluetoothDevices( 317 @Nullable LocalBluetoothManager manager, boolean inSharingOnly) { 318 if (manager == null) { 319 Log.w(TAG, "getConnectedBluetoothDevices(): LocalBluetoothManager is null!"); 320 return emptyList(); 321 } 322 var leBroadcastAssistant = getLeBroadcastAssistant(manager); 323 if (leBroadcastAssistant == null) { 324 Log.w(TAG, "getConnectedBluetoothDevices(): LeBroadcastAssistant is null!"); 325 return emptyList(); 326 } 327 List<BluetoothDevice> connectedDevices = leBroadcastAssistant.getAllConnectedDevices(); 328 Optional<CachedBluetoothDevice> cachedBluetoothDevice = 329 inSharingOnly 330 ? getCachedBluetoothDeviceInSharing(manager) 331 : getCachedBluetoothDeviceInSharingOrLeConnected(manager); 332 List<BluetoothDevice> bluetoothDevices = 333 cachedBluetoothDevice 334 .map( 335 c -> 336 Stream.concat( 337 Stream.of(c.getDevice()), 338 c.getMemberDevice().stream() 339 .map( 340 CachedBluetoothDevice 341 ::getDevice)) 342 .filter(connectedDevices::contains) 343 .toList()) 344 .orElse(emptyList()); 345 Log.d(TAG, "getConnectedBluetoothDevices() devices: " + bluetoothDevices); 346 return bluetoothDevices; 347 } 348 getLeBroadcastAssistant( @ullable LocalBluetoothManager manager)349 private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant( 350 @Nullable LocalBluetoothManager manager) { 351 if (manager == null) { 352 Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!"); 353 return null; 354 } 355 356 LocalBluetoothProfileManager profileManager = manager.getProfileManager(); 357 if (profileManager == null) { 358 Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!"); 359 return null; 360 } 361 362 return profileManager.getLeAudioBroadcastAssistantProfile(); 363 } 364 getBroadcastName(BluetoothLeBroadcastMetadata source)365 static String getBroadcastName(BluetoothLeBroadcastMetadata source) { 366 String broadcastName = source.getBroadcastName(); 367 if (broadcastName != null && !broadcastName.isEmpty()) { 368 return broadcastName; 369 } 370 return source.getSubgroups().stream() 371 .map(subgroup -> subgroup.getContentMetadata().getProgramInfo()) 372 .filter(programInfo -> !Strings.isNullOrEmpty(programInfo)) 373 .findFirst() 374 .orElse("Broadcast Id: " + source.getBroadcastId()); 375 } 376 getBroadcastName(BluetoothLeBroadcastReceiveState state)377 static String getBroadcastName(BluetoothLeBroadcastReceiveState state) { 378 return state.getSubgroupMetadata().stream() 379 .map(BluetoothLeAudioContentMetadata::getProgramInfo) 380 .filter(i -> !Strings.isNullOrEmpty(i)) 381 .findFirst() 382 .orElse("Broadcast Id: " + state.getBroadcastId()); 383 } 384 startMediaService(Context context, int audioStreamBroadcastId, String title)385 void startMediaService(Context context, int audioStreamBroadcastId, String title) { 386 List<BluetoothDevice> devices = 387 getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true); 388 if (devices.isEmpty()) { 389 return; 390 } 391 var intent = new Intent(context, AudioStreamMediaService.class); 392 intent.putExtra(BROADCAST_ID, audioStreamBroadcastId); 393 intent.putExtra(BROADCAST_TITLE, title); 394 intent.putParcelableArrayListExtra(DEVICES, new ArrayList<>(devices)); 395 context.startService(intent); 396 } 397 configureAppBarByOrientation(@ullable FragmentActivity activity)398 static void configureAppBarByOrientation(@Nullable FragmentActivity activity) { 399 if (activity != null) { 400 AppBarLayout appBarLayout = activity.findViewById(R.id.app_bar); 401 if (appBarLayout != null) { 402 boolean canAppBarExpand = 403 activity.getResources().getConfiguration().orientation 404 == Configuration.ORIENTATION_PORTRAIT; 405 appBarLayout.setExpanded(canAppBarExpand); 406 } 407 } 408 } 409 410 /** 411 * Retrieves a set of enabled screen reader services that are pre-installed. 412 * 413 * <p>This method checks the accessibility manager for enabled accessibility services 414 * and filters them based on a list of pre-installed screen reader service component names 415 * defined in the {@code config_preinstalled_screen_reader_services} resource array.</p> 416 * 417 * @param context The context. 418 * @return A set of {@link ComponentName} objects representing the enabled pre-installed 419 * screen reader services, or an empty set if no services are found, or if an error occurs. 420 */ getEnabledScreenReaderServices(Context context)421 public static Set<ComponentName> getEnabledScreenReaderServices(Context context) { 422 AccessibilityManager manager = context.getSystemService(AccessibilityManager.class); 423 if (manager == null) { 424 return Collections.emptySet(); 425 } 426 Set<String> screenReaderServices = new HashSet<>(); 427 Collections.addAll(screenReaderServices, context.getResources() 428 .getStringArray(R.array.config_preinstalled_screen_reader_services)); 429 if (screenReaderServices.isEmpty()) { 430 return Collections.emptySet(); 431 } 432 Set<ComponentName> enabledScreenReaderServices = new HashSet<>(); 433 List<AccessibilityServiceInfo> enabledServices = manager.getEnabledAccessibilityServiceList( 434 AccessibilityServiceInfo.FEEDBACK_ALL_MASK); 435 for (AccessibilityServiceInfo service : enabledServices) { 436 ComponentName componentName = service.getComponentName(); 437 if (screenReaderServices.contains(componentName.flattenToString())) { 438 enabledScreenReaderServices.add(componentName); 439 } 440 } 441 Log.d(TAG, "getEnabledScreenReaderServices(): " + enabledScreenReaderServices); 442 return enabledScreenReaderServices; 443 } 444 445 /** 446 * Turns off the specified accessibility services. 447 * 448 * This method iterates through a set of ComponentName objects, each representing an 449 * accessibility service, and disables them. 450 * 451 * @param context The application context. 452 * @param services A set of ComponentName objects representing the services to disable. 453 */ setAccessibilityServiceOff(Context context, Set<ComponentName> services)454 public static void setAccessibilityServiceOff(Context context, Set<ComponentName> services) { 455 for (ComponentName service : services) { 456 Log.d(TAG, "setScreenReaderOff(): " + service); 457 setAccessibilityServiceState(context, service, false); 458 } 459 } 460 } 461