1 package com.android.settingslib.bluetooth; 2 3 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.UNKNOWN_VALUE_PLACEHOLDER; 4 import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix; 5 import static com.android.settingslib.widget.AdaptiveOutlineDrawable.ICON_TYPE_ADVANCED; 6 7 import android.annotation.SuppressLint; 8 import android.bluetooth.BluetoothAdapter; 9 import android.bluetooth.BluetoothClass; 10 import android.bluetooth.BluetoothCsipSetCoordinator; 11 import android.bluetooth.BluetoothDevice; 12 import android.bluetooth.BluetoothLeBroadcastReceiveState; 13 import android.bluetooth.BluetoothProfile; 14 import android.bluetooth.BluetoothStatusCodes; 15 import android.content.ComponentName; 16 import android.content.ContentResolver; 17 import android.content.Context; 18 import android.content.Intent; 19 import android.content.pm.ApplicationInfo; 20 import android.content.pm.PackageManager; 21 import android.content.res.Resources; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.drawable.BitmapDrawable; 25 import android.graphics.drawable.Drawable; 26 import android.hardware.input.InputManager; 27 import android.media.AudioDeviceAttributes; 28 import android.media.AudioDeviceInfo; 29 import android.media.AudioManager; 30 import android.net.Uri; 31 import android.provider.DeviceConfig; 32 import android.provider.MediaStore; 33 import android.provider.Settings; 34 import android.sysprop.BluetoothProperties; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.util.Pair; 38 import android.view.InputDevice; 39 40 import androidx.annotation.DrawableRes; 41 import androidx.annotation.NonNull; 42 import androidx.annotation.Nullable; 43 import androidx.annotation.WorkerThread; 44 import androidx.core.graphics.drawable.IconCompat; 45 46 import com.android.settingslib.R; 47 import com.android.settingslib.flags.Flags; 48 import com.android.settingslib.widget.AdaptiveIcon; 49 import com.android.settingslib.widget.AdaptiveOutlineDrawable; 50 51 import com.google.common.collect.ImmutableSet; 52 53 import java.io.IOException; 54 import java.lang.reflect.InvocationTargetException; 55 import java.lang.reflect.Method; 56 import java.util.List; 57 import java.util.Locale; 58 import java.util.Objects; 59 import java.util.Optional; 60 import java.util.Set; 61 import java.util.regex.Matcher; 62 import java.util.regex.Pattern; 63 import java.util.stream.Collectors; 64 65 public class BluetoothUtils { 66 private static final String TAG = "BluetoothUtils"; 67 68 public static final boolean V = false; // verbose logging 69 public static final boolean D = true; // regular logging 70 71 public static final int META_INT_ERROR = -1; 72 public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled"; 73 public static final String DEVELOPER_OPTION_PREVIEW_KEY = 74 "bluetooth_le_audio_sharing_ui_preview_enabled"; 75 private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; 76 private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH"; 77 private static final Set<Integer> SA_PROFILES = 78 ImmutableSet.of( 79 BluetoothProfile.A2DP, BluetoothProfile.LE_AUDIO, BluetoothProfile.HEARING_AID); 80 private static final List<Integer> BLUETOOTH_DEVICE_CLASS_HEADSET = 81 List.of( 82 BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES, 83 BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET); 84 85 private static final String TEMP_BOND_TYPE = "TEMP_BOND_TYPE"; 86 private static final String TEMP_BOND_DEVICE_METADATA_VALUE = "le_audio_sharing"; 87 88 private static ErrorListener sErrorListener; 89 getConnectionStateSummary(int connectionState)90 public static int getConnectionStateSummary(int connectionState) { 91 switch (connectionState) { 92 case BluetoothProfile.STATE_CONNECTED: 93 return R.string.bluetooth_connected; 94 case BluetoothProfile.STATE_CONNECTING: 95 return R.string.bluetooth_connecting; 96 case BluetoothProfile.STATE_DISCONNECTED: 97 return R.string.bluetooth_disconnected; 98 case BluetoothProfile.STATE_DISCONNECTING: 99 return R.string.bluetooth_disconnecting; 100 default: 101 return 0; 102 } 103 } 104 showError(Context context, String name, int messageResId)105 static void showError(Context context, String name, int messageResId) { 106 if (sErrorListener != null) { 107 sErrorListener.onShowError(context, name, messageResId); 108 } 109 } 110 setErrorListener(ErrorListener listener)111 public static void setErrorListener(ErrorListener listener) { 112 sErrorListener = listener; 113 } 114 115 public interface ErrorListener { onShowError(Context context, String name, int messageResId)116 void onShowError(Context context, String name, int messageResId); 117 } 118 119 /** 120 * @param context to access resources from 121 * @param cachedDevice to get class from 122 * @return pair containing the drawable and the description of the type of the device. The type 123 * could either derived from metadata or CoD. 124 */ getDerivedBtClassDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)125 public static Pair<Drawable, String> getDerivedBtClassDrawableWithDescription( 126 Context context, CachedBluetoothDevice cachedDevice) { 127 return BluetoothUtils.isAdvancedUntetheredDevice(cachedDevice.getDevice()) 128 ? new Pair<>( 129 getBluetoothDrawable( 130 context, com.android.internal.R.drawable.ic_bt_headphones_a2dp), 131 context.getString(R.string.bluetooth_talkback_headphone)) 132 : BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice); 133 } 134 135 /** 136 * @param context to access resources from 137 * @param cachedDevice to get class from 138 * @return pair containing the drawable and the description of the Bluetooth class of the 139 * device. 140 */ getBtClassDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)141 public static Pair<Drawable, String> getBtClassDrawableWithDescription( 142 Context context, CachedBluetoothDevice cachedDevice) { 143 BluetoothClass btClass = cachedDevice.getBtClass(); 144 if (btClass != null) { 145 switch (btClass.getMajorDeviceClass()) { 146 case BluetoothClass.Device.Major.COMPUTER: 147 return new Pair<>( 148 getBluetoothDrawable( 149 context, com.android.internal.R.drawable.ic_bt_laptop), 150 context.getString(R.string.bluetooth_talkback_computer)); 151 152 case BluetoothClass.Device.Major.PHONE: 153 return new Pair<>( 154 getBluetoothDrawable(context, com.android.internal.R.drawable.ic_phone), 155 context.getString(R.string.bluetooth_talkback_phone)); 156 157 case BluetoothClass.Device.Major.PERIPHERAL: 158 return new Pair<>( 159 getBluetoothDrawable(context, HidProfile.getHidClassDrawable(btClass)), 160 context.getString(R.string.bluetooth_talkback_input_peripheral)); 161 162 case BluetoothClass.Device.Major.IMAGING: 163 return new Pair<>( 164 getBluetoothDrawable( 165 context, com.android.internal.R.drawable.ic_settings_print), 166 context.getString(R.string.bluetooth_talkback_imaging)); 167 168 default: 169 // unrecognized device class; continue 170 } 171 } 172 173 if (cachedDevice.isHearingAidDevice()) { 174 return new Pair<>( 175 getBluetoothDrawable( 176 context, com.android.internal.R.drawable.ic_bt_hearing_aid), 177 context.getString(R.string.bluetooth_talkback_hearing_aids)); 178 } 179 180 List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles(); 181 int resId = 0; 182 for (LocalBluetoothProfile profile : profiles) { 183 int profileResId = profile.getDrawableResource(btClass); 184 if (profileResId != 0) { 185 // The device should show hearing aid icon if it contains any hearing aid related 186 // profiles 187 if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) { 188 return new Pair<>( 189 getBluetoothDrawable(context, profileResId), 190 context.getString(R.string.bluetooth_talkback_hearing_aids)); 191 } 192 if (resId == 0) { 193 resId = profileResId; 194 } 195 } 196 } 197 if (resId != 0) { 198 return new Pair<>(getBluetoothDrawable(context, resId), null); 199 } 200 201 if (btClass != null) { 202 if (doesClassMatch(btClass, BluetoothClass.PROFILE_HEADSET)) { 203 return new Pair<>( 204 getBluetoothDrawable( 205 context, com.android.internal.R.drawable.ic_bt_headset_hfp), 206 context.getString(R.string.bluetooth_talkback_headset)); 207 } 208 if (doesClassMatch(btClass, BluetoothClass.PROFILE_A2DP)) { 209 return new Pair<>( 210 getBluetoothDrawable( 211 context, com.android.internal.R.drawable.ic_bt_headphones_a2dp), 212 context.getString(R.string.bluetooth_talkback_headphone)); 213 } 214 } 215 return new Pair<>( 216 getBluetoothDrawable(context, com.android.internal.R.drawable.ic_settings_bluetooth) 217 .mutate(), 218 context.getString(R.string.bluetooth_talkback_bluetooth)); 219 } 220 221 /** Get bluetooth drawable by {@code resId} */ getBluetoothDrawable(Context context, @DrawableRes int resId)222 public static Drawable getBluetoothDrawable(Context context, @DrawableRes int resId) { 223 return context.getDrawable(resId); 224 } 225 226 /** Get colorful bluetooth icon with description */ getBtRainbowDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)227 public static Pair<Drawable, String> getBtRainbowDrawableWithDescription( 228 Context context, CachedBluetoothDevice cachedDevice) { 229 final Resources resources = context.getResources(); 230 final Pair<Drawable, String> pair = 231 BluetoothUtils.getBtDrawableWithDescription(context, cachedDevice); 232 233 if (pair.first instanceof BitmapDrawable) { 234 return new Pair<>( 235 new AdaptiveOutlineDrawable( 236 resources, ((BitmapDrawable) pair.first).getBitmap()), 237 pair.second); 238 } 239 240 int hashCode; 241 if ((cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)) { 242 hashCode = new Integer(cachedDevice.getGroupId()).hashCode(); 243 } else { 244 hashCode = cachedDevice.getAddress().hashCode(); 245 } 246 247 return new Pair<>(buildBtRainbowDrawable(context, pair.first, hashCode), pair.second); 248 } 249 250 /** Build Bluetooth device icon with rainbow */ buildBtRainbowDrawable( Context context, Drawable drawable, int hashCode)251 private static Drawable buildBtRainbowDrawable( 252 Context context, Drawable drawable, int hashCode) { 253 final Resources resources = context.getResources(); 254 255 // Deal with normal headset 256 final int[] iconFgColors = resources.getIntArray(R.array.bt_icon_fg_colors); 257 final int[] iconBgColors = resources.getIntArray(R.array.bt_icon_bg_colors); 258 259 // get color index based on mac address 260 final int index = Math.abs(hashCode % iconBgColors.length); 261 drawable.setTint(iconFgColors[index]); 262 final Drawable adaptiveIcon = new AdaptiveIcon(context, drawable); 263 ((AdaptiveIcon) adaptiveIcon).setBackgroundColor(iconBgColors[index]); 264 265 return adaptiveIcon; 266 } 267 268 /** Get bluetooth icon with description */ getBtDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)269 public static Pair<Drawable, String> getBtDrawableWithDescription( 270 Context context, CachedBluetoothDevice cachedDevice) { 271 final Pair<Drawable, String> pair = 272 BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice); 273 final BluetoothDevice bluetoothDevice = cachedDevice.getDevice(); 274 final int iconSize = 275 context.getResources().getDimensionPixelSize(R.dimen.bt_nearby_icon_size); 276 final Resources resources = context.getResources(); 277 278 // Deal with advanced device icon 279 if (isAdvancedDetailsHeader(bluetoothDevice)) { 280 final Uri iconUri = getUriMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON); 281 if (iconUri != null) { 282 try { 283 context.getContentResolver() 284 .takePersistableUriPermission( 285 iconUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); 286 } catch (SecurityException e) { 287 Log.e(TAG, "Failed to take persistable permission for: " + iconUri, e); 288 } 289 try { 290 final Bitmap bitmap = 291 MediaStore.Images.Media.getBitmap( 292 context.getContentResolver(), iconUri); 293 if (bitmap != null) { 294 final Bitmap resizedBitmap = 295 Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false); 296 bitmap.recycle(); 297 return new Pair<>( 298 new BitmapDrawable(resources, resizedBitmap), pair.second); 299 } 300 } catch (IOException e) { 301 Log.e(TAG, "Failed to get drawable for: " + iconUri, e); 302 } catch (SecurityException e) { 303 Log.e(TAG, "Failed to get permission for: " + iconUri, e); 304 } 305 } 306 } 307 308 return new Pair<>(pair.first, pair.second); 309 } 310 311 /** 312 * Check if the Bluetooth device supports advanced metadata 313 * 314 * @param bluetoothDevice the BluetoothDevice to get metadata 315 * @return true if it supports advanced metadata, false otherwise. 316 */ isAdvancedDetailsHeader(@onNull BluetoothDevice bluetoothDevice)317 public static boolean isAdvancedDetailsHeader(@NonNull BluetoothDevice bluetoothDevice) { 318 if (!isAdvancedHeaderEnabled()) { 319 return false; 320 } 321 if (isUntetheredHeadset(bluetoothDevice)) { 322 return true; 323 } 324 if (Flags.enableDeterminingAdvancedDetailsHeaderWithMetadata()) { 325 // A FastPair device that use advanced details header must have METADATA_MAIN_ICON 326 if (getUriMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON) != null) { 327 Log.d(TAG, "isAdvancedDetailsHeader is true with main icon uri"); 328 return true; 329 } 330 return false; 331 } 332 // The metadata is for Android S 333 String deviceType = 334 getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE); 335 if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET) 336 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_WATCH) 337 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_DEFAULT) 338 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_STYLUS)) { 339 Log.d(TAG, "isAdvancedDetailsHeader: deviceType is " + deviceType); 340 return true; 341 } 342 return false; 343 } 344 345 /** 346 * Check if the Bluetooth device is supports advanced metadata and an untethered headset 347 * 348 * @param bluetoothDevice the BluetoothDevice to get metadata 349 * @return true if it supports advanced metadata and an untethered headset, false otherwise. 350 */ isAdvancedUntetheredDevice(@onNull BluetoothDevice bluetoothDevice)351 public static boolean isAdvancedUntetheredDevice(@NonNull BluetoothDevice bluetoothDevice) { 352 if (!isAdvancedHeaderEnabled()) { 353 return false; 354 } 355 if (isUntetheredHeadset(bluetoothDevice)) { 356 return true; 357 } 358 if (!Flags.enableDeterminingAdvancedDetailsHeaderWithMetadata()) { 359 // The METADATA_IS_UNTETHERED_HEADSET of an untethered FastPair headset is always true, 360 // so there's no need to check the device type. 361 String deviceType = 362 getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE); 363 if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) { 364 Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device"); 365 return true; 366 } 367 } 368 return false; 369 } 370 371 /** 372 * Check if a device class matches with a defined BluetoothClass device. 373 * 374 * @param device Must be one of the public constants in {@link BluetoothClass.Device} 375 * @return true if device class matches, false otherwise. 376 */ isDeviceClassMatched( @onNull BluetoothDevice bluetoothDevice, int device)377 public static boolean isDeviceClassMatched( 378 @NonNull BluetoothDevice bluetoothDevice, int device) { 379 final BluetoothClass bluetoothClass = bluetoothDevice.getBluetoothClass(); 380 return bluetoothClass != null && bluetoothClass.getDeviceClass() == device; 381 } 382 isAdvancedHeaderEnabled()383 private static boolean isAdvancedHeaderEnabled() { 384 if (!DeviceConfig.getBoolean( 385 DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED, true)) { 386 Log.d(TAG, "isAdvancedDetailsHeader: advancedEnabled is false"); 387 return false; 388 } 389 return true; 390 } 391 isUntetheredHeadset(@onNull BluetoothDevice bluetoothDevice)392 private static boolean isUntetheredHeadset(@NonNull BluetoothDevice bluetoothDevice) { 393 // The metadata is for Android R 394 if (getBooleanMetaData(bluetoothDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { 395 Log.d(TAG, "isAdvancedDetailsHeader: untetheredHeadset is true"); 396 return true; 397 } 398 return false; 399 } 400 401 /** Checks whether the bluetooth device is a headset. */ isHeadset(@onNull BluetoothDevice bluetoothDevice)402 public static boolean isHeadset(@NonNull BluetoothDevice bluetoothDevice) { 403 String deviceType = 404 BluetoothUtils.getStringMetaData( 405 bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE); 406 if (!TextUtils.isEmpty(deviceType)) { 407 return BluetoothDevice.DEVICE_TYPE_HEADSET.equals(deviceType) 408 || BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.equals(deviceType); 409 } 410 BluetoothClass btClass = bluetoothDevice.getBluetoothClass(); 411 return btClass != null && BLUETOOTH_DEVICE_CLASS_HEADSET.contains(btClass.getDeviceClass()); 412 } 413 414 /** Create an Icon pointing to a drawable. */ createIconWithDrawable(Drawable drawable)415 public static IconCompat createIconWithDrawable(Drawable drawable) { 416 Bitmap bitmap; 417 if (drawable instanceof BitmapDrawable) { 418 bitmap = ((BitmapDrawable) drawable).getBitmap(); 419 } else { 420 final int width = drawable.getIntrinsicWidth(); 421 final int height = drawable.getIntrinsicHeight(); 422 bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1); 423 } 424 return IconCompat.createWithBitmap(bitmap); 425 } 426 427 /** Build device icon with advanced outline */ buildAdvancedDrawable(Context context, Drawable drawable)428 public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) { 429 final int iconSize = 430 context.getResources().getDimensionPixelSize(R.dimen.advanced_icon_size); 431 final Resources resources = context.getResources(); 432 433 Bitmap bitmap = null; 434 if (drawable instanceof BitmapDrawable) { 435 bitmap = ((BitmapDrawable) drawable).getBitmap(); 436 } else { 437 final int width = drawable.getIntrinsicWidth(); 438 final int height = drawable.getIntrinsicHeight(); 439 bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1); 440 } 441 442 if (bitmap != null) { 443 final Bitmap resizedBitmap = 444 Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false); 445 bitmap.recycle(); 446 return new AdaptiveOutlineDrawable(resources, resizedBitmap, ICON_TYPE_ADVANCED); 447 } 448 449 return drawable; 450 } 451 452 /** Creates a drawable with specified width and height. */ createBitmap(Drawable drawable, int width, int height)453 public static Bitmap createBitmap(Drawable drawable, int width, int height) { 454 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 455 final Canvas canvas = new Canvas(bitmap); 456 drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 457 drawable.draw(canvas); 458 return bitmap; 459 } 460 461 /** 462 * Get boolean Bluetooth metadata 463 * 464 * @param bluetoothDevice the BluetoothDevice to get metadata 465 * @param key key value within the list of BluetoothDevice.METADATA_* 466 * @return the boolean metdata 467 */ getBooleanMetaData(BluetoothDevice bluetoothDevice, int key)468 public static boolean getBooleanMetaData(BluetoothDevice bluetoothDevice, int key) { 469 if (bluetoothDevice == null) { 470 return false; 471 } 472 final byte[] data = bluetoothDevice.getMetadata(key); 473 if (data == null) { 474 return false; 475 } 476 return Boolean.parseBoolean(new String(data)); 477 } 478 479 /** 480 * Get String Bluetooth metadata 481 * 482 * @param bluetoothDevice the BluetoothDevice to get metadata 483 * @param key key value within the list of BluetoothDevice.METADATA_* 484 * @return the String metdata 485 */ getStringMetaData(BluetoothDevice bluetoothDevice, int key)486 public static String getStringMetaData(BluetoothDevice bluetoothDevice, int key) { 487 if (bluetoothDevice == null) { 488 return null; 489 } 490 final byte[] data = bluetoothDevice.getMetadata(key); 491 if (data == null) { 492 return null; 493 } 494 return new String(data); 495 } 496 497 /** 498 * Get integer Bluetooth metadata 499 * 500 * @param bluetoothDevice the BluetoothDevice to get metadata 501 * @param key key value within the list of BluetoothDevice.METADATA_* 502 * @return the int metdata 503 */ getIntMetaData(BluetoothDevice bluetoothDevice, int key)504 public static int getIntMetaData(BluetoothDevice bluetoothDevice, int key) { 505 if (bluetoothDevice == null) { 506 return META_INT_ERROR; 507 } 508 final byte[] data = bluetoothDevice.getMetadata(key); 509 if (data == null) { 510 return META_INT_ERROR; 511 } 512 try { 513 return Integer.parseInt(new String(data)); 514 } catch (NumberFormatException e) { 515 return META_INT_ERROR; 516 } 517 } 518 519 /** 520 * Get URI Bluetooth metadata 521 * 522 * @param bluetoothDevice the BluetoothDevice to get metadata 523 * @param key key value within the list of BluetoothDevice.METADATA_* 524 * @return the URI metdata 525 */ getUriMetaData(BluetoothDevice bluetoothDevice, int key)526 public static Uri getUriMetaData(BluetoothDevice bluetoothDevice, int key) { 527 String data = getStringMetaData(bluetoothDevice, key); 528 if (data == null) { 529 return null; 530 } 531 return Uri.parse(data); 532 } 533 534 /** 535 * Gets string metadata from Fast Pair customized fields. 536 * 537 * @param bluetoothDevice the BluetoothDevice to get metadata 538 * @return the string metadata 539 */ 540 @Nullable getFastPairCustomizedField( @ullable BluetoothDevice bluetoothDevice, @NonNull String key)541 public static String getFastPairCustomizedField( 542 @Nullable BluetoothDevice bluetoothDevice, @NonNull String key) { 543 String data = getStringMetaData(bluetoothDevice, METADATA_FAST_PAIR_CUSTOMIZED_FIELDS); 544 return extraTagValue(key, data); 545 } 546 547 /** 548 * Get URI Bluetooth metadata for extra control 549 * 550 * @param bluetoothDevice the BluetoothDevice to get metadata 551 * @return the URI metadata 552 */ getControlUriMetaData(BluetoothDevice bluetoothDevice)553 public static String getControlUriMetaData(BluetoothDevice bluetoothDevice) { 554 return getFastPairCustomizedField(bluetoothDevice, KEY_HEARABLE_CONTROL_SLICE); 555 } 556 557 /** 558 * Check if the Bluetooth device is an AvailableMediaBluetoothDevice, which means: 1) currently 559 * connected 2) is Hearing Aid or LE Audio OR 3) connected profile matches currentAudioProfile 560 * 561 * @param cachedDevice the CachedBluetoothDevice 562 * @param isOngoingCall get the current audio profile based on if in phone call 563 * @return if the device is AvailableMediaBluetoothDevice 564 */ 565 @WorkerThread isAvailableMediaBluetoothDevice( CachedBluetoothDevice cachedDevice, boolean isOngoingCall)566 public static boolean isAvailableMediaBluetoothDevice( 567 CachedBluetoothDevice cachedDevice, boolean isOngoingCall) { 568 int currentAudioProfile; 569 570 if (isOngoingCall) { 571 currentAudioProfile = BluetoothProfile.HEADSET; 572 } else { 573 currentAudioProfile = BluetoothProfile.A2DP; 574 } 575 576 boolean isFilterMatched = false; 577 if (isDeviceConnected(cachedDevice)) { 578 // If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP. 579 // It would show in Available Devices group. 580 if (cachedDevice.isConnectedAshaHearingAidDevice() 581 || cachedDevice.isConnectedLeAudioDevice()) { 582 Log.d( 583 TAG, 584 "isFilterMatched() device : " 585 + cachedDevice.getName() 586 + ", the profile is connected."); 587 return true; 588 } 589 // According to the current audio profile type, 590 // this page will show the bluetooth device that have corresponding profile. 591 // For example: 592 // If current audio profile is a2dp, show the bluetooth device that have a2dp profile. 593 // If current audio profile is headset, 594 // show the bluetooth device that have headset profile. 595 switch (currentAudioProfile) { 596 case BluetoothProfile.A2DP: 597 isFilterMatched = cachedDevice.isConnectedA2dpDevice(); 598 break; 599 case BluetoothProfile.HEADSET: 600 isFilterMatched = cachedDevice.isConnectedHfpDevice(); 601 break; 602 } 603 } 604 return isFilterMatched; 605 } 606 607 /** 608 * Checks if a given `CachedBluetoothDevice` is available for audio sharing and being switch as 609 * active media device. 610 * 611 * <p>This method determines if the device meets the following criteria to be available: 612 * 613 * <ol> 614 * <li>Audio sharing session is off. 615 * <li>The device is one of the two connected devices on the LE Broadcast Assistant profile. 616 * <li>The device is not currently active on the LE Audio profile. 617 * <li>There is exactly one other device that is active on the LE Audio profile. 618 * </ol> 619 * 620 * @param cachedDevice The `CachedBluetoothDevice` to check. 621 * @param localBluetoothManager The `LocalBluetoothManager` instance, or null if unavailable. 622 * @return `true` if the device is available for audio sharing and settings as active, `false` 623 * otherwise. 624 */ 625 @WorkerThread isAvailableAudioSharingMediaBluetoothDevice( CachedBluetoothDevice cachedDevice, @Nullable LocalBluetoothManager localBluetoothManager)626 public static boolean isAvailableAudioSharingMediaBluetoothDevice( 627 CachedBluetoothDevice cachedDevice, 628 @Nullable LocalBluetoothManager localBluetoothManager) { 629 LocalBluetoothLeBroadcastAssistant assistantProfile = 630 Optional.ofNullable(localBluetoothManager) 631 .map(LocalBluetoothManager::getProfileManager) 632 .map(LocalBluetoothProfileManager::getLeAudioBroadcastAssistantProfile) 633 .orElse(null); 634 LeAudioProfile leAudioProfile = 635 Optional.ofNullable(localBluetoothManager) 636 .map(LocalBluetoothManager::getProfileManager) 637 .map(LocalBluetoothProfileManager::getLeAudioProfile) 638 .orElse(null); 639 CachedBluetoothDeviceManager deviceManager = 640 Optional.ofNullable(localBluetoothManager) 641 .map(LocalBluetoothManager::getCachedDeviceManager) 642 .orElse(null); 643 // If any of the profiles are null, or broadcast is already on, return false 644 if (assistantProfile == null 645 || leAudioProfile == null 646 || deviceManager == null 647 || isBroadcasting(localBluetoothManager)) { 648 return false; 649 } 650 Set<Integer> connectedGroupIds = 651 assistantProfile.getAllConnectedDevices().stream() 652 .map(deviceManager::findDevice) 653 .filter(Objects::nonNull) 654 .map(BluetoothUtils::getGroupId) 655 .collect(Collectors.toSet()); 656 Set<Integer> activeGroupIds = 657 leAudioProfile.getActiveDevices().stream() 658 .map(deviceManager::findDevice) 659 .filter(Objects::nonNull) 660 .map(BluetoothUtils::getGroupId) 661 .collect(Collectors.toSet()); 662 int groupId = getGroupId(cachedDevice); 663 return activeGroupIds.size() == 1 664 && !activeGroupIds.contains(groupId) 665 && connectedGroupIds.size() == 2 666 && connectedGroupIds.contains(groupId); 667 } 668 669 /** Returns if the le audio sharing UI is available. */ isAudioSharingUIAvailable(@ullable Context context)670 public static boolean isAudioSharingUIAvailable(@Nullable Context context) { 671 return (Flags.enableLeAudioSharing() 672 || (context != null && Flags.audioSharingDeveloperOption() 673 && getAudioSharingPreviewValue(context.getContentResolver()))) 674 && isAudioSharingSupported(); 675 } 676 677 /** Returns if the le audio sharing hysteresis mode fix is available. */ 678 @WorkerThread isAudioSharingHysteresisModeFixAvailable(@ullable Context context)679 public static boolean isAudioSharingHysteresisModeFixAvailable(@Nullable Context context) { 680 return (audioSharingHysteresisModeFix() && Flags.enableLeAudioSharing()) 681 || (context != null && Flags.audioSharingDeveloperOption() 682 && getAudioSharingPreviewValue(context.getContentResolver())); 683 } 684 685 /** Returns if the le audio sharing is enabled. */ isAudioSharingEnabled()686 public static boolean isAudioSharingEnabled() { 687 return Flags.enableLeAudioSharing() && isAudioSharingSupported(); 688 } 689 690 /** Returns if the le audio sharing preview is enabled in developer option. */ isAudioSharingPreviewEnabled(@ullable ContentResolver contentResolver)691 public static boolean isAudioSharingPreviewEnabled(@Nullable ContentResolver contentResolver) { 692 return Flags.audioSharingDeveloperOption() 693 && getAudioSharingPreviewValue(contentResolver) 694 && isAudioSharingSupported(); 695 } 696 697 /** Returns if the device has le audio sharing capability */ isAudioSharingSupported()698 private static boolean isAudioSharingSupported() { 699 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 700 try { 701 // b/381777424 The APIs have to return ERROR_BLUETOOTH_NOT_ENABLED when BT off based on 702 // CDD definition. 703 // However, app layer need to gate the feature based on whether the device has audio 704 // sharing capability regardless of the BT state. 705 // So here we check the BluetoothProperties when BT off. 706 // 707 // TODO: Also check SystemProperties "persist.bluetooth.leaudio_dynamic_switcher.mode" 708 // and return true if it is in broadcast mode. 709 // Now SystemUI don't have access to read the value. 710 int sourceSupportedCode = adapter.isLeAudioBroadcastSourceSupported(); 711 int assistantSupportedCode = adapter.isLeAudioBroadcastAssistantSupported(); 712 return (sourceSupportedCode == BluetoothStatusCodes.FEATURE_SUPPORTED 713 || (sourceSupportedCode == BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED 714 && BluetoothProperties.isProfileBapBroadcastSourceEnabled().orElse(false))) 715 && (assistantSupportedCode == BluetoothStatusCodes.FEATURE_SUPPORTED 716 || (assistantSupportedCode == BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED 717 && BluetoothProperties.isProfileBapBroadcastAssistEnabled().orElse(false))); 718 } catch (IllegalStateException e) { 719 Log.d(TAG, "Fail to check isAudioSharingSupported, e = ", e); 720 return false; 721 } 722 } 723 724 /** Check if the {@link CachedBluetoothDevice} is a media device */ 725 @WorkerThread isMediaDevice(@ullable CachedBluetoothDevice cachedDevice)726 public static boolean isMediaDevice(@Nullable CachedBluetoothDevice cachedDevice) { 727 if (cachedDevice == null) return false; 728 return cachedDevice.getProfiles().stream() 729 .anyMatch( 730 profile -> 731 profile instanceof A2dpProfile 732 || profile instanceof HearingAidProfile 733 || profile instanceof LeAudioProfile 734 || profile instanceof HeadsetProfile); 735 } 736 737 /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */ 738 @WorkerThread isLeAudioSupported(@ullable CachedBluetoothDevice cachedDevice)739 public static boolean isLeAudioSupported(@Nullable CachedBluetoothDevice cachedDevice) { 740 if (cachedDevice == null) return false; 741 return cachedDevice.getProfiles().stream() 742 .anyMatch( 743 profile -> 744 profile instanceof LeAudioProfile 745 && profile.isEnabled(cachedDevice.getDevice())); 746 } 747 748 /** Returns if the broadcast is on-going. */ 749 @WorkerThread isBroadcasting(@ullable LocalBluetoothManager manager)750 public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) { 751 if (manager == null) return false; 752 LocalBluetoothLeBroadcast broadcast = 753 manager.getProfileManager().getLeAudioBroadcastProfile(); 754 return broadcast != null && broadcast.isEnabled(null); 755 } 756 757 /** 758 * Check if {@link CachedBluetoothDevice} (lead or member) has connected to a broadcast source. 759 * 760 * @param cachedDevice The cached bluetooth device to check. 761 * @param localBtManager The BT manager to provide BT functions. 762 * @return Whether the device has connected to a broadcast source. 763 */ 764 @WorkerThread hasConnectedBroadcastSource( @ullable CachedBluetoothDevice cachedDevice, @Nullable LocalBluetoothManager localBtManager)765 public static boolean hasConnectedBroadcastSource( 766 @Nullable CachedBluetoothDevice cachedDevice, 767 @Nullable LocalBluetoothManager localBtManager) { 768 if (cachedDevice == null) return false; 769 if (hasConnectedBroadcastSourceForBtDevice(cachedDevice.getDevice(), localBtManager)) { 770 Log.d( 771 TAG, 772 "Lead device has connected broadcast source, device = " 773 + cachedDevice.getDevice().getAnonymizedAddress()); 774 return true; 775 } 776 // Return true if member device is in broadcast. 777 for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) { 778 if (hasConnectedBroadcastSourceForBtDevice(device.getDevice(), localBtManager)) { 779 Log.d( 780 TAG, 781 "Member device has connected broadcast source, device = " 782 + device.getDevice().getAnonymizedAddress()); 783 return true; 784 } 785 } 786 return false; 787 } 788 789 /** 790 * Check if {@link BluetoothDevice} has connected to a broadcast source. 791 * 792 * @param device The bluetooth device to check. 793 * @param localBtManager The BT manager to provide BT functions. 794 * @return Whether the device has connected to a broadcast source. 795 */ 796 @WorkerThread hasConnectedBroadcastSourceForBtDevice( @ullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager)797 public static boolean hasConnectedBroadcastSourceForBtDevice( 798 @Nullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager) { 799 if (localBtManager == null) { 800 Log.d(TAG, "Skip check hasConnectedBroadcastSourceForBtDevice due to arg is null"); 801 return false; 802 } 803 if (isAudioSharingHysteresisModeFixAvailable(localBtManager.getContext())) { 804 return hasActiveLocalBroadcastSourceForBtDevice(device, localBtManager); 805 } 806 LocalBluetoothLeBroadcastAssistant assistant = 807 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 808 if (device == null || assistant == null) { 809 Log.d(TAG, "Skip check hasConnectedBroadcastSourceForBtDevice due to arg is null"); 810 return false; 811 } 812 List<BluetoothLeBroadcastReceiveState> sourceList = assistant.getAllSources(device); 813 return !sourceList.isEmpty() && sourceList.stream().anyMatch(BluetoothUtils::isConnected); 814 } 815 816 /** 817 * Check if {@link BluetoothDevice} has a active local broadcast source. 818 * 819 * @param device The bluetooth device to check. 820 * @param localBtManager The BT manager to provide BT functions. 821 * @return Whether the device has a active local broadcast source. 822 */ 823 @WorkerThread hasActiveLocalBroadcastSourceForBtDevice( @ullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager)824 public static boolean hasActiveLocalBroadcastSourceForBtDevice( 825 @Nullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager) { 826 LocalBluetoothLeBroadcastAssistant assistant = 827 localBtManager == null 828 ? null 829 : localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 830 LocalBluetoothLeBroadcast broadcast = 831 localBtManager == null 832 ? null 833 : localBtManager.getProfileManager().getLeAudioBroadcastProfile(); 834 if (device == null || assistant == null || broadcast == null) { 835 Log.d(TAG, "Skip check hasActiveLocalBroadcastSourceForBtDevice due to arg is null"); 836 return false; 837 } 838 List<BluetoothLeBroadcastReceiveState> sourceList = assistant.getAllSources(device); 839 int broadcastId = broadcast.getLatestBroadcastId(); 840 return !sourceList.isEmpty() 841 && broadcastId != UNKNOWN_VALUE_PLACEHOLDER 842 && sourceList.stream().anyMatch(source -> isSourceMatched(source, broadcastId)); 843 } 844 845 /** Checks the connectivity status based on the provided broadcast receive state. */ 846 @WorkerThread isConnected(BluetoothLeBroadcastReceiveState state)847 public static boolean isConnected(BluetoothLeBroadcastReceiveState state) { 848 return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0); 849 } 850 851 /** Checks if the broadcast id is matched based on the provided broadcast receive state. */ 852 @WorkerThread isSourceMatched( @ullable BluetoothLeBroadcastReceiveState state, int broadcastId)853 public static boolean isSourceMatched( 854 @Nullable BluetoothLeBroadcastReceiveState state, int broadcastId) { 855 return state != null && state.getBroadcastId() == broadcastId; 856 } 857 858 /** 859 * Checks if the Bluetooth device is an available hearing device, which means: 1) currently 860 * connected 2) is Hearing Aid 3) connected profile match hearing aid related profiles (e.g. 861 * ASHA, HAP) 862 * 863 * @param cachedDevice the CachedBluetoothDevice 864 * @return if the device is Available hearing device 865 */ 866 @WorkerThread isAvailableHearingDevice(CachedBluetoothDevice cachedDevice)867 public static boolean isAvailableHearingDevice(CachedBluetoothDevice cachedDevice) { 868 if (isDeviceConnected(cachedDevice) && cachedDevice.isConnectedHearingAidDevice()) { 869 Log.d( 870 TAG, 871 "isFilterMatched() device : " 872 + cachedDevice.getName() 873 + ", the profile is connected."); 874 return true; 875 } 876 return false; 877 } 878 879 /** 880 * Check if the Bluetooth device is a ConnectedBluetoothDevice, which means: 1) currently 881 * connected 2) is not Hearing Aid or LE Audio AND 3) connected profile does not match 882 * currentAudioProfile 883 * 884 * @param cachedDevice the CachedBluetoothDevice 885 * @param isOngoingCall get the current audio profile based on if in phone call 886 * @return if the device is AvailableMediaBluetoothDevice 887 */ 888 @WorkerThread isConnectedBluetoothDevice( CachedBluetoothDevice cachedDevice, boolean isOngoingCall)889 public static boolean isConnectedBluetoothDevice( 890 CachedBluetoothDevice cachedDevice, boolean isOngoingCall) { 891 int currentAudioProfile; 892 893 if (isOngoingCall) { 894 currentAudioProfile = BluetoothProfile.HEADSET; 895 } else { 896 currentAudioProfile = BluetoothProfile.A2DP; 897 } 898 899 boolean isFilterMatched = false; 900 if (isDeviceConnected(cachedDevice)) { 901 // If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP. 902 // It would not show in Connected Devices group. 903 if (cachedDevice.isConnectedAshaHearingAidDevice() 904 || cachedDevice.isConnectedLeAudioDevice()) { 905 return false; 906 } 907 // According to the current audio profile type, 908 // this page will show the bluetooth device that doesn't have corresponding profile. 909 // For example: 910 // If current audio profile is a2dp, 911 // show the bluetooth device that doesn't have a2dp profile. 912 // If current audio profile is headset, 913 // show the bluetooth device that doesn't have headset profile. 914 switch (currentAudioProfile) { 915 case BluetoothProfile.A2DP: 916 isFilterMatched = !cachedDevice.isConnectedA2dpDevice(); 917 break; 918 case BluetoothProfile.HEADSET: 919 isFilterMatched = !cachedDevice.isConnectedHfpDevice(); 920 break; 921 } 922 } 923 return isFilterMatched; 924 } 925 926 /** 927 * Check if the Bluetooth device is an active media device 928 * 929 * @param cachedDevice the CachedBluetoothDevice 930 * @return if the Bluetooth device is an active media device 931 */ isActiveMediaDevice(CachedBluetoothDevice cachedDevice)932 public static boolean isActiveMediaDevice(CachedBluetoothDevice cachedDevice) { 933 return cachedDevice.isActiveDevice(BluetoothProfile.A2DP) 934 || cachedDevice.isActiveDevice(BluetoothProfile.HEADSET) 935 || cachedDevice.isActiveDevice(BluetoothProfile.HEARING_AID) 936 || cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO); 937 } 938 939 /** 940 * Check if the Bluetooth device is an active LE Audio device 941 * 942 * @param cachedDevice the CachedBluetoothDevice 943 * @return if the Bluetooth device is an active LE Audio device 944 */ isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice)945 public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) { 946 return cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO); 947 } 948 isDeviceConnected(CachedBluetoothDevice cachedDevice)949 private static boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) { 950 if (cachedDevice == null) { 951 return false; 952 } 953 final BluetoothDevice device = cachedDevice.getDevice(); 954 return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected(); 955 } 956 957 @SuppressLint("NewApi") // Hidden API made public doesClassMatch(BluetoothClass btClass, int classId)958 private static boolean doesClassMatch(BluetoothClass btClass, int classId) { 959 return btClass.doesClassMatch(classId); 960 } 961 extraTagValue(String tag, String metaData)962 private static String extraTagValue(String tag, String metaData) { 963 if (TextUtils.isEmpty(metaData)) { 964 return null; 965 } 966 Pattern pattern = Pattern.compile(generateExpressionWithTag(tag, "(.*?)")); 967 Matcher matcher = pattern.matcher(metaData); 968 if (matcher.find()) { 969 return matcher.group(1); 970 } 971 return null; 972 } 973 getTagStart(String tag)974 private static String getTagStart(String tag) { 975 return String.format(Locale.ENGLISH, "<%s>", tag); 976 } 977 getTagEnd(String tag)978 private static String getTagEnd(String tag) { 979 return String.format(Locale.ENGLISH, "</%s>", tag); 980 } 981 generateExpressionWithTag(String tag, String value)982 private static String generateExpressionWithTag(String tag, String value) { 983 return getTagStart(tag) + value + getTagEnd(tag); 984 } 985 986 /** 987 * Returns the BluetoothDevice's exclusive manager ({@link 988 * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists, otherwise null. 989 */ 990 @Nullable getExclusiveManager(BluetoothDevice bluetoothDevice)991 private static String getExclusiveManager(BluetoothDevice bluetoothDevice) { 992 byte[] exclusiveManagerBytes = 993 bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER); 994 if (exclusiveManagerBytes == null) { 995 Log.d( 996 TAG, 997 "Bluetooth device " 998 + bluetoothDevice.getName() 999 + " doesn't have exclusive manager"); 1000 return null; 1001 } 1002 return new String(exclusiveManagerBytes); 1003 } 1004 1005 /** Checks if given package is installed and enabled */ isPackageInstalledAndEnabled(Context context, String packageName)1006 private static boolean isPackageInstalledAndEnabled(Context context, String packageName) { 1007 PackageManager packageManager = context.getPackageManager(); 1008 try { 1009 ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0); 1010 return appInfo.enabled; 1011 } catch (PackageManager.NameNotFoundException e) { 1012 Log.d(TAG, "Package " + packageName + " is not installed/enabled"); 1013 } 1014 return false; 1015 } 1016 1017 /** 1018 * A BluetoothDevice is exclusively managed if 1) it has field {@link 1019 * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata. 2) the exclusive manager app is 1020 * installed and enabled. 1021 */ isExclusivelyManagedBluetoothDevice( @onNull Context context, @NonNull BluetoothDevice bluetoothDevice)1022 public static boolean isExclusivelyManagedBluetoothDevice( 1023 @NonNull Context context, @NonNull BluetoothDevice bluetoothDevice) { 1024 String exclusiveManagerName = getExclusiveManager(bluetoothDevice); 1025 if (exclusiveManagerName == null) { 1026 return false; 1027 } 1028 1029 ComponentName exclusiveManagerComponent = 1030 ComponentName.unflattenFromString(exclusiveManagerName); 1031 String exclusiveManagerPackage = 1032 exclusiveManagerComponent != null 1033 ? exclusiveManagerComponent.getPackageName() 1034 : exclusiveManagerName; 1035 1036 if (!isPackageInstalledAndEnabled(context, exclusiveManagerPackage)) { 1037 return false; 1038 } else { 1039 Log.d(TAG, "Found exclusively managed app " + exclusiveManagerPackage); 1040 return true; 1041 } 1042 } 1043 1044 /** 1045 * Get CSIP group id for {@link CachedBluetoothDevice}. 1046 * 1047 * <p>If CachedBluetoothDevice#getGroupId is invalid, fetch group id from 1048 * LeAudioProfile#getGroupId. 1049 */ getGroupId(@ullable CachedBluetoothDevice cachedDevice)1050 public static int getGroupId(@Nullable CachedBluetoothDevice cachedDevice) { 1051 if (cachedDevice == null) return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 1052 int groupId = cachedDevice.getGroupId(); 1053 String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress(); 1054 if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 1055 Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress); 1056 return groupId; 1057 } 1058 for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) { 1059 if (profile instanceof LeAudioProfile) { 1060 Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress); 1061 return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice()); 1062 } 1063 } 1064 Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress); 1065 return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 1066 } 1067 1068 /** Get primary device Uri in broadcast. */ 1069 @NonNull getPrimaryGroupIdUriForBroadcast()1070 public static String getPrimaryGroupIdUriForBroadcast() { 1071 // TODO: once API is stable, deprecate SettingsProvider solution 1072 return "bluetooth_le_broadcast_fallback_active_group_id"; 1073 } 1074 1075 /** Get primary device group id in broadcast from SettingsProvider. */ 1076 @WorkerThread getPrimaryGroupIdForBroadcast(@onNull ContentResolver contentResolver)1077 public static int getPrimaryGroupIdForBroadcast(@NonNull ContentResolver contentResolver) { 1078 // TODO: once API is stable, deprecate SettingsProvider solution 1079 return Settings.Secure.getInt( 1080 contentResolver, 1081 getPrimaryGroupIdUriForBroadcast(), 1082 BluetoothCsipSetCoordinator.GROUP_ID_INVALID); 1083 } 1084 1085 /** 1086 * Get primary device group id in broadcast. 1087 * 1088 * If Flags.adoptPrimaryGroupManagementApiV2 is enabled, get group id by API, 1089 * Otherwise, still get value from SettingsProvider. 1090 */ 1091 @WorkerThread getPrimaryGroupIdForBroadcast(@onNull ContentResolver contentResolver, @Nullable LocalBluetoothManager manager)1092 public static int getPrimaryGroupIdForBroadcast(@NonNull ContentResolver contentResolver, 1093 @Nullable LocalBluetoothManager manager) { 1094 if (Flags.adoptPrimaryGroupManagementApiV2()) { 1095 LeAudioProfile leaProfile = manager == null ? null : 1096 manager.getProfileManager().getLeAudioProfile(); 1097 if (leaProfile == null) { 1098 Log.d(TAG, "getPrimaryGroupIdForBroadcast: profile is null"); 1099 return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 1100 } 1101 return leaProfile.getBroadcastToUnicastFallbackGroup(); 1102 } else { 1103 return getPrimaryGroupIdForBroadcast(contentResolver); 1104 } 1105 } 1106 1107 /** Get develop option value for audio sharing preview. */ 1108 @WorkerThread getAudioSharingPreviewValue(@ullable ContentResolver contentResolver)1109 public static boolean getAudioSharingPreviewValue(@Nullable ContentResolver contentResolver) { 1110 if (contentResolver == null) return false; 1111 return Settings.Global.getInt( 1112 contentResolver, 1113 DEVELOPER_OPTION_PREVIEW_KEY, 1114 0 // value off 1115 ) == 1; 1116 } 1117 1118 /** Get secondary {@link CachedBluetoothDevice} in broadcast. */ 1119 @Nullable 1120 @WorkerThread getSecondaryDeviceForBroadcast( @onNull ContentResolver contentResolver, @Nullable LocalBluetoothManager localBtManager)1121 public static CachedBluetoothDevice getSecondaryDeviceForBroadcast( 1122 @NonNull ContentResolver contentResolver, 1123 @Nullable LocalBluetoothManager localBtManager) { 1124 if (localBtManager == null) return null; 1125 LocalBluetoothLeBroadcast broadcast = 1126 localBtManager.getProfileManager().getLeAudioBroadcastProfile(); 1127 if (broadcast == null || !broadcast.isEnabled(null)) return null; 1128 int primaryGroupId = getPrimaryGroupIdForBroadcast(contentResolver, localBtManager); 1129 if (primaryGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) return null; 1130 LocalBluetoothLeBroadcastAssistant assistant = 1131 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 1132 CachedBluetoothDeviceManager deviceManager = localBtManager.getCachedDeviceManager(); 1133 List<BluetoothDevice> devices = assistant.getAllConnectedDevices(); 1134 for (BluetoothDevice device : devices) { 1135 CachedBluetoothDevice cachedDevice = deviceManager.findDevice(device); 1136 if (hasConnectedBroadcastSource(cachedDevice, localBtManager)) { 1137 int groupId = getGroupId(cachedDevice); 1138 if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID 1139 && groupId != primaryGroupId) { 1140 return cachedDevice; 1141 } 1142 } 1143 } 1144 return null; 1145 } 1146 1147 /** 1148 * Gets {@link AudioDeviceAttributes} of bluetooth device for spatial audio. Returns null if 1149 * it's not an audio device(no A2DP, LE Audio and Hearing Aid profile). 1150 */ 1151 @Nullable getAudioDeviceAttributesForSpatialAudio( CachedBluetoothDevice cachedDevice, @AudioManager.AudioDeviceCategory int audioDeviceCategory)1152 public static AudioDeviceAttributes getAudioDeviceAttributesForSpatialAudio( 1153 CachedBluetoothDevice cachedDevice, 1154 @AudioManager.AudioDeviceCategory int audioDeviceCategory) { 1155 AudioDeviceAttributes saDevice = null; 1156 for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) { 1157 // pick first enabled profile that is compatible with spatial audio 1158 if (SA_PROFILES.contains(profile.getProfileId()) 1159 && profile.isEnabled(cachedDevice.getDevice())) { 1160 switch (profile.getProfileId()) { 1161 case BluetoothProfile.A2DP: 1162 saDevice = 1163 new AudioDeviceAttributes( 1164 AudioDeviceAttributes.ROLE_OUTPUT, 1165 AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, 1166 cachedDevice.getAddress()); 1167 break; 1168 case BluetoothProfile.LE_AUDIO: 1169 if (audioDeviceCategory == AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER) { 1170 saDevice = 1171 new AudioDeviceAttributes( 1172 AudioDeviceAttributes.ROLE_OUTPUT, 1173 AudioDeviceInfo.TYPE_BLE_SPEAKER, 1174 cachedDevice.getAddress()); 1175 } else { 1176 saDevice = 1177 new AudioDeviceAttributes( 1178 AudioDeviceAttributes.ROLE_OUTPUT, 1179 AudioDeviceInfo.TYPE_BLE_HEADSET, 1180 cachedDevice.getAddress()); 1181 } 1182 1183 break; 1184 case BluetoothProfile.HEARING_AID: 1185 saDevice = 1186 new AudioDeviceAttributes( 1187 AudioDeviceAttributes.ROLE_OUTPUT, 1188 AudioDeviceInfo.TYPE_HEARING_AID, 1189 cachedDevice.getAddress()); 1190 break; 1191 default: 1192 Log.i( 1193 TAG, 1194 "unrecognized profile for spatial audio: " 1195 + profile.getProfileId()); 1196 break; 1197 } 1198 break; 1199 } 1200 } 1201 return saDevice; 1202 } 1203 1204 /** 1205 * Verifies if the device is temporary bond in audio sharing. 1206 * 1207 * @param bluetoothDevice the BluetoothDevice to verify 1208 * @return if the device is temporary bond 1209 */ isTemporaryBondDevice(@ullable BluetoothDevice bluetoothDevice)1210 public static boolean isTemporaryBondDevice(@Nullable BluetoothDevice bluetoothDevice) { 1211 String metadataValue = getFastPairCustomizedField(bluetoothDevice, TEMP_BOND_TYPE); 1212 return Objects.equals(metadataValue, TEMP_BOND_DEVICE_METADATA_VALUE); 1213 } 1214 1215 /** 1216 * Set temp bond metadata to device 1217 * 1218 * @param device the BluetoothDevice to be marked as temp bond 1219 * 1220 * Note: It is a workaround since Bluetooth API is not ready. 1221 * Avoid using this method if possible 1222 */ setTemporaryBondMetadata(@ullable BluetoothDevice device)1223 public static void setTemporaryBondMetadata(@Nullable BluetoothDevice device) { 1224 if (device == null) return; 1225 if (!Flags.enableTemporaryBondDevicesUi()) { 1226 Log.d(TAG, "Skip setTemporaryBondMetadata, flag is disabled"); 1227 return; 1228 } 1229 String fastPairCustomizedMeta = getStringMetaData(device, 1230 METADATA_FAST_PAIR_CUSTOMIZED_FIELDS); 1231 String fullContentWithTag = generateExpressionWithTag(TEMP_BOND_TYPE, 1232 TEMP_BOND_DEVICE_METADATA_VALUE); 1233 if (TextUtils.isEmpty(fastPairCustomizedMeta)) { 1234 fastPairCustomizedMeta = fullContentWithTag; 1235 } else { 1236 String oldValue = extraTagValue(TEMP_BOND_TYPE, fastPairCustomizedMeta); 1237 if (TextUtils.isEmpty(oldValue)) { 1238 fastPairCustomizedMeta += fullContentWithTag; 1239 } else { 1240 fastPairCustomizedMeta = 1241 fastPairCustomizedMeta.replace( 1242 generateExpressionWithTag(TEMP_BOND_TYPE, oldValue), 1243 fullContentWithTag); 1244 } 1245 } 1246 device.setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, fastPairCustomizedMeta.getBytes()); 1247 } 1248 1249 /** 1250 * Returns the {@link InputDevice} of the given bluetooth address if the device is a input 1251 * device. 1252 * 1253 * @param address The address of the bluetooth device 1254 * @return The {@link InputDevice} of the given address if applicable 1255 */ 1256 @Nullable getInputDevice(Context context, String address)1257 public static InputDevice getInputDevice(Context context, String address) { 1258 InputManager im = context.getSystemService(InputManager.class); 1259 1260 if (im != null) { 1261 for (int deviceId : im.getInputDeviceIds()) { 1262 String btAddress = im.getInputDeviceBluetoothAddress(deviceId); 1263 1264 if (btAddress != null && btAddress.equals(address)) { 1265 return im.getInputDevice(deviceId); 1266 } 1267 } 1268 } 1269 return null; 1270 } 1271 1272 /** 1273 * Identifies whether a device is a stylus using the associated {@link InputDevice} or 1274 * {@link CachedBluetoothDevice}. 1275 * InputDevices are only available when the device is USI or Bluetooth-connected, whereas 1276 * CachedBluetoothDevices are available for Bluetooth devices when connected or paired, 1277 * so to handle all cases, both are needed. 1278 * 1279 * @param inputDevice The associated input device of the stylus 1280 * @param cachedBluetoothDevice The associated bluetooth device of the stylus 1281 */ isDeviceStylus(@ullable InputDevice inputDevice, @Nullable CachedBluetoothDevice cachedBluetoothDevice)1282 public static boolean isDeviceStylus(@Nullable InputDevice inputDevice, 1283 @Nullable CachedBluetoothDevice cachedBluetoothDevice) { 1284 if (inputDevice != null && inputDevice.supportsSource(InputDevice.SOURCE_STYLUS)) { 1285 return true; 1286 } 1287 1288 if (cachedBluetoothDevice != null) { 1289 BluetoothDevice bluetoothDevice = cachedBluetoothDevice.getDevice(); 1290 String deviceType = BluetoothUtils.getStringMetaData(bluetoothDevice, 1291 BluetoothDevice.METADATA_DEVICE_TYPE); 1292 return TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_STYLUS); 1293 } 1294 1295 return false; 1296 } 1297 1298 /** Gets key missing count of the device. This is a workaround before the API is rolled out. */ getKeyMissingCount(BluetoothDevice device)1299 public static Integer getKeyMissingCount(BluetoothDevice device) { 1300 try { 1301 Method m = BluetoothDevice.class.getDeclaredMethod("getKeyMissingCount"); 1302 return (int) m.invoke(device); 1303 } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { 1304 Log.w(TAG, "error happens when getKeyMissingCount."); 1305 return null; 1306 } 1307 } 1308 } 1309