1 package com.android.settingslib.bluetooth; 2 3 import static com.android.settingslib.widget.AdaptiveOutlineDrawable.ICON_TYPE_ADVANCED; 4 5 import android.annotation.SuppressLint; 6 import android.bluetooth.BluetoothAdapter; 7 import android.bluetooth.BluetoothClass; 8 import android.bluetooth.BluetoothCsipSetCoordinator; 9 import android.bluetooth.BluetoothDevice; 10 import android.bluetooth.BluetoothLeBroadcastReceiveState; 11 import android.bluetooth.BluetoothProfile; 12 import android.bluetooth.BluetoothStatusCodes; 13 import android.content.ComponentName; 14 import android.content.Context; 15 import android.content.Intent; 16 import android.content.pm.ApplicationInfo; 17 import android.content.pm.PackageManager; 18 import android.content.res.Resources; 19 import android.graphics.Bitmap; 20 import android.graphics.Canvas; 21 import android.graphics.drawable.BitmapDrawable; 22 import android.graphics.drawable.Drawable; 23 import android.media.AudioManager; 24 import android.net.Uri; 25 import android.provider.DeviceConfig; 26 import android.provider.MediaStore; 27 import android.text.TextUtils; 28 import android.util.Log; 29 import android.util.Pair; 30 31 import androidx.annotation.DrawableRes; 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 import androidx.annotation.WorkerThread; 35 import androidx.core.graphics.drawable.IconCompat; 36 37 import com.android.settingslib.R; 38 import com.android.settingslib.flags.Flags; 39 import com.android.settingslib.widget.AdaptiveIcon; 40 import com.android.settingslib.widget.AdaptiveOutlineDrawable; 41 42 import java.io.IOException; 43 import java.util.List; 44 import java.util.Locale; 45 import java.util.regex.Matcher; 46 import java.util.regex.Pattern; 47 48 public class BluetoothUtils { 49 private static final String TAG = "BluetoothUtils"; 50 51 public static final boolean V = false; // verbose logging 52 public static final boolean D = true; // regular logging 53 54 public static final int META_INT_ERROR = -1; 55 public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled"; 56 private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; 57 private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH"; 58 59 private static ErrorListener sErrorListener; 60 getConnectionStateSummary(int connectionState)61 public static int getConnectionStateSummary(int connectionState) { 62 switch (connectionState) { 63 case BluetoothProfile.STATE_CONNECTED: 64 return R.string.bluetooth_connected; 65 case BluetoothProfile.STATE_CONNECTING: 66 return R.string.bluetooth_connecting; 67 case BluetoothProfile.STATE_DISCONNECTED: 68 return R.string.bluetooth_disconnected; 69 case BluetoothProfile.STATE_DISCONNECTING: 70 return R.string.bluetooth_disconnecting; 71 default: 72 return 0; 73 } 74 } 75 showError(Context context, String name, int messageResId)76 static void showError(Context context, String name, int messageResId) { 77 if (sErrorListener != null) { 78 sErrorListener.onShowError(context, name, messageResId); 79 } 80 } 81 setErrorListener(ErrorListener listener)82 public static void setErrorListener(ErrorListener listener) { 83 sErrorListener = listener; 84 } 85 86 public interface ErrorListener { onShowError(Context context, String name, int messageResId)87 void onShowError(Context context, String name, int messageResId); 88 } 89 90 /** 91 * @param context to access resources from 92 * @param cachedDevice to get class from 93 * @return pair containing the drawable and the description of the Bluetooth class of the 94 * device. 95 */ getBtClassDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)96 public static Pair<Drawable, String> getBtClassDrawableWithDescription( 97 Context context, CachedBluetoothDevice cachedDevice) { 98 BluetoothClass btClass = cachedDevice.getBtClass(); 99 if (btClass != null) { 100 switch (btClass.getMajorDeviceClass()) { 101 case BluetoothClass.Device.Major.COMPUTER: 102 return new Pair<>( 103 getBluetoothDrawable( 104 context, com.android.internal.R.drawable.ic_bt_laptop), 105 context.getString(R.string.bluetooth_talkback_computer)); 106 107 case BluetoothClass.Device.Major.PHONE: 108 return new Pair<>( 109 getBluetoothDrawable(context, com.android.internal.R.drawable.ic_phone), 110 context.getString(R.string.bluetooth_talkback_phone)); 111 112 case BluetoothClass.Device.Major.PERIPHERAL: 113 return new Pair<>( 114 getBluetoothDrawable(context, HidProfile.getHidClassDrawable(btClass)), 115 context.getString(R.string.bluetooth_talkback_input_peripheral)); 116 117 case BluetoothClass.Device.Major.IMAGING: 118 return new Pair<>( 119 getBluetoothDrawable( 120 context, com.android.internal.R.drawable.ic_settings_print), 121 context.getString(R.string.bluetooth_talkback_imaging)); 122 123 default: 124 // unrecognized device class; continue 125 } 126 } 127 128 if (cachedDevice.isHearingAidDevice()) { 129 return new Pair<>( 130 getBluetoothDrawable( 131 context, com.android.internal.R.drawable.ic_bt_hearing_aid), 132 context.getString(R.string.bluetooth_talkback_hearing_aids)); 133 } 134 135 List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles(); 136 int resId = 0; 137 for (LocalBluetoothProfile profile : profiles) { 138 int profileResId = profile.getDrawableResource(btClass); 139 if (profileResId != 0) { 140 // The device should show hearing aid icon if it contains any hearing aid related 141 // profiles 142 if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) { 143 return new Pair<>( 144 getBluetoothDrawable(context, profileResId), 145 context.getString(R.string.bluetooth_talkback_hearing_aids)); 146 } 147 if (resId == 0) { 148 resId = profileResId; 149 } 150 } 151 } 152 if (resId != 0) { 153 return new Pair<>(getBluetoothDrawable(context, resId), null); 154 } 155 156 if (btClass != null) { 157 if (doesClassMatch(btClass, BluetoothClass.PROFILE_HEADSET)) { 158 return new Pair<>( 159 getBluetoothDrawable( 160 context, com.android.internal.R.drawable.ic_bt_headset_hfp), 161 context.getString(R.string.bluetooth_talkback_headset)); 162 } 163 if (doesClassMatch(btClass, BluetoothClass.PROFILE_A2DP)) { 164 return new Pair<>( 165 getBluetoothDrawable( 166 context, com.android.internal.R.drawable.ic_bt_headphones_a2dp), 167 context.getString(R.string.bluetooth_talkback_headphone)); 168 } 169 } 170 return new Pair<>( 171 getBluetoothDrawable(context, com.android.internal.R.drawable.ic_settings_bluetooth) 172 .mutate(), 173 context.getString(R.string.bluetooth_talkback_bluetooth)); 174 } 175 176 /** Get bluetooth drawable by {@code resId} */ getBluetoothDrawable(Context context, @DrawableRes int resId)177 public static Drawable getBluetoothDrawable(Context context, @DrawableRes int resId) { 178 return context.getDrawable(resId); 179 } 180 181 /** Get colorful bluetooth icon with description */ getBtRainbowDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)182 public static Pair<Drawable, String> getBtRainbowDrawableWithDescription( 183 Context context, CachedBluetoothDevice cachedDevice) { 184 final Resources resources = context.getResources(); 185 final Pair<Drawable, String> pair = 186 BluetoothUtils.getBtDrawableWithDescription(context, cachedDevice); 187 188 if (pair.first instanceof BitmapDrawable) { 189 return new Pair<>( 190 new AdaptiveOutlineDrawable( 191 resources, ((BitmapDrawable) pair.first).getBitmap()), 192 pair.second); 193 } 194 195 int hashCode; 196 if ((cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)) { 197 hashCode = new Integer(cachedDevice.getGroupId()).hashCode(); 198 } else { 199 hashCode = cachedDevice.getAddress().hashCode(); 200 } 201 202 return new Pair<>(buildBtRainbowDrawable(context, pair.first, hashCode), pair.second); 203 } 204 205 /** Build Bluetooth device icon with rainbow */ buildBtRainbowDrawable( Context context, Drawable drawable, int hashCode)206 private static Drawable buildBtRainbowDrawable( 207 Context context, Drawable drawable, int hashCode) { 208 final Resources resources = context.getResources(); 209 210 // Deal with normal headset 211 final int[] iconFgColors = resources.getIntArray(R.array.bt_icon_fg_colors); 212 final int[] iconBgColors = resources.getIntArray(R.array.bt_icon_bg_colors); 213 214 // get color index based on mac address 215 final int index = Math.abs(hashCode % iconBgColors.length); 216 drawable.setTint(iconFgColors[index]); 217 final Drawable adaptiveIcon = new AdaptiveIcon(context, drawable); 218 ((AdaptiveIcon) adaptiveIcon).setBackgroundColor(iconBgColors[index]); 219 220 return adaptiveIcon; 221 } 222 223 /** Get bluetooth icon with description */ getBtDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)224 public static Pair<Drawable, String> getBtDrawableWithDescription( 225 Context context, CachedBluetoothDevice cachedDevice) { 226 final Pair<Drawable, String> pair = 227 BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice); 228 final BluetoothDevice bluetoothDevice = cachedDevice.getDevice(); 229 final int iconSize = 230 context.getResources().getDimensionPixelSize(R.dimen.bt_nearby_icon_size); 231 final Resources resources = context.getResources(); 232 233 // Deal with advanced device icon 234 if (isAdvancedDetailsHeader(bluetoothDevice)) { 235 final Uri iconUri = getUriMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON); 236 if (iconUri != null) { 237 try { 238 context.getContentResolver() 239 .takePersistableUriPermission( 240 iconUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); 241 } catch (SecurityException e) { 242 Log.e(TAG, "Failed to take persistable permission for: " + iconUri, e); 243 } 244 try { 245 final Bitmap bitmap = 246 MediaStore.Images.Media.getBitmap( 247 context.getContentResolver(), iconUri); 248 if (bitmap != null) { 249 final Bitmap resizedBitmap = 250 Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false); 251 bitmap.recycle(); 252 return new Pair<>( 253 new BitmapDrawable(resources, resizedBitmap), pair.second); 254 } 255 } catch (IOException e) { 256 Log.e(TAG, "Failed to get drawable for: " + iconUri, e); 257 } catch (SecurityException e) { 258 Log.e(TAG, "Failed to get permission for: " + iconUri, e); 259 } 260 } 261 } 262 263 return new Pair<>(pair.first, pair.second); 264 } 265 266 /** 267 * Check if the Bluetooth device supports advanced metadata 268 * 269 * @param bluetoothDevice the BluetoothDevice to get metadata 270 * @return true if it supports advanced metadata, false otherwise. 271 */ isAdvancedDetailsHeader(@onNull BluetoothDevice bluetoothDevice)272 public static boolean isAdvancedDetailsHeader(@NonNull BluetoothDevice bluetoothDevice) { 273 if (!isAdvancedHeaderEnabled()) { 274 return false; 275 } 276 if (isUntetheredHeadset(bluetoothDevice)) { 277 return true; 278 } 279 if (Flags.enableDeterminingAdvancedDetailsHeaderWithMetadata()) { 280 // A FastPair device that use advanced details header must have METADATA_MAIN_ICON 281 if (getUriMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON) != null) { 282 Log.d(TAG, "isAdvancedDetailsHeader is true with main icon uri"); 283 return true; 284 } 285 return false; 286 } 287 // The metadata is for Android S 288 String deviceType = 289 getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE); 290 if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET) 291 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_WATCH) 292 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_DEFAULT) 293 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_STYLUS)) { 294 Log.d(TAG, "isAdvancedDetailsHeader: deviceType is " + deviceType); 295 return true; 296 } 297 return false; 298 } 299 300 /** 301 * Check if the Bluetooth device is supports advanced metadata and an untethered headset 302 * 303 * @param bluetoothDevice the BluetoothDevice to get metadata 304 * @return true if it supports advanced metadata and an untethered headset, false otherwise. 305 */ isAdvancedUntetheredDevice(@onNull BluetoothDevice bluetoothDevice)306 public static boolean isAdvancedUntetheredDevice(@NonNull BluetoothDevice bluetoothDevice) { 307 if (!isAdvancedHeaderEnabled()) { 308 return false; 309 } 310 if (isUntetheredHeadset(bluetoothDevice)) { 311 return true; 312 } 313 if (!Flags.enableDeterminingAdvancedDetailsHeaderWithMetadata()) { 314 // The METADATA_IS_UNTETHERED_HEADSET of an untethered FastPair headset is always true, 315 // so there's no need to check the device type. 316 String deviceType = 317 getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE); 318 if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) { 319 Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device"); 320 return true; 321 } 322 } 323 return false; 324 } 325 326 /** 327 * Check if a device class matches with a defined BluetoothClass device. 328 * 329 * @param device Must be one of the public constants in {@link BluetoothClass.Device} 330 * @return true if device class matches, false otherwise. 331 */ isDeviceClassMatched( @onNull BluetoothDevice bluetoothDevice, int device)332 public static boolean isDeviceClassMatched( 333 @NonNull BluetoothDevice bluetoothDevice, int device) { 334 final BluetoothClass bluetoothClass = bluetoothDevice.getBluetoothClass(); 335 return bluetoothClass != null && bluetoothClass.getDeviceClass() == device; 336 } 337 isAdvancedHeaderEnabled()338 private static boolean isAdvancedHeaderEnabled() { 339 if (!DeviceConfig.getBoolean( 340 DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED, true)) { 341 Log.d(TAG, "isAdvancedDetailsHeader: advancedEnabled is false"); 342 return false; 343 } 344 return true; 345 } 346 isUntetheredHeadset(@onNull BluetoothDevice bluetoothDevice)347 private static boolean isUntetheredHeadset(@NonNull BluetoothDevice bluetoothDevice) { 348 // The metadata is for Android R 349 if (getBooleanMetaData(bluetoothDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { 350 Log.d(TAG, "isAdvancedDetailsHeader: untetheredHeadset is true"); 351 return true; 352 } 353 return false; 354 } 355 356 /** Create an Icon pointing to a drawable. */ createIconWithDrawable(Drawable drawable)357 public static IconCompat createIconWithDrawable(Drawable drawable) { 358 Bitmap bitmap; 359 if (drawable instanceof BitmapDrawable) { 360 bitmap = ((BitmapDrawable) drawable).getBitmap(); 361 } else { 362 final int width = drawable.getIntrinsicWidth(); 363 final int height = drawable.getIntrinsicHeight(); 364 bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1); 365 } 366 return IconCompat.createWithBitmap(bitmap); 367 } 368 369 /** Build device icon with advanced outline */ buildAdvancedDrawable(Context context, Drawable drawable)370 public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) { 371 final int iconSize = 372 context.getResources().getDimensionPixelSize(R.dimen.advanced_icon_size); 373 final Resources resources = context.getResources(); 374 375 Bitmap bitmap = null; 376 if (drawable instanceof BitmapDrawable) { 377 bitmap = ((BitmapDrawable) drawable).getBitmap(); 378 } else { 379 final int width = drawable.getIntrinsicWidth(); 380 final int height = drawable.getIntrinsicHeight(); 381 bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1); 382 } 383 384 if (bitmap != null) { 385 final Bitmap resizedBitmap = 386 Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false); 387 bitmap.recycle(); 388 return new AdaptiveOutlineDrawable(resources, resizedBitmap, ICON_TYPE_ADVANCED); 389 } 390 391 return drawable; 392 } 393 394 /** Creates a drawable with specified width and height. */ createBitmap(Drawable drawable, int width, int height)395 public static Bitmap createBitmap(Drawable drawable, int width, int height) { 396 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 397 final Canvas canvas = new Canvas(bitmap); 398 drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 399 drawable.draw(canvas); 400 return bitmap; 401 } 402 403 /** 404 * Get boolean Bluetooth metadata 405 * 406 * @param bluetoothDevice the BluetoothDevice to get metadata 407 * @param key key value within the list of BluetoothDevice.METADATA_* 408 * @return the boolean metdata 409 */ getBooleanMetaData(BluetoothDevice bluetoothDevice, int key)410 public static boolean getBooleanMetaData(BluetoothDevice bluetoothDevice, int key) { 411 if (bluetoothDevice == null) { 412 return false; 413 } 414 final byte[] data = bluetoothDevice.getMetadata(key); 415 if (data == null) { 416 return false; 417 } 418 return Boolean.parseBoolean(new String(data)); 419 } 420 421 /** 422 * Get String Bluetooth metadata 423 * 424 * @param bluetoothDevice the BluetoothDevice to get metadata 425 * @param key key value within the list of BluetoothDevice.METADATA_* 426 * @return the String metdata 427 */ getStringMetaData(BluetoothDevice bluetoothDevice, int key)428 public static String getStringMetaData(BluetoothDevice bluetoothDevice, int key) { 429 if (bluetoothDevice == null) { 430 return null; 431 } 432 final byte[] data = bluetoothDevice.getMetadata(key); 433 if (data == null) { 434 return null; 435 } 436 return new String(data); 437 } 438 439 /** 440 * Get integer Bluetooth metadata 441 * 442 * @param bluetoothDevice the BluetoothDevice to get metadata 443 * @param key key value within the list of BluetoothDevice.METADATA_* 444 * @return the int metdata 445 */ getIntMetaData(BluetoothDevice bluetoothDevice, int key)446 public static int getIntMetaData(BluetoothDevice bluetoothDevice, int key) { 447 if (bluetoothDevice == null) { 448 return META_INT_ERROR; 449 } 450 final byte[] data = bluetoothDevice.getMetadata(key); 451 if (data == null) { 452 return META_INT_ERROR; 453 } 454 try { 455 return Integer.parseInt(new String(data)); 456 } catch (NumberFormatException e) { 457 return META_INT_ERROR; 458 } 459 } 460 461 /** 462 * Get URI 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 URI metdata 467 */ getUriMetaData(BluetoothDevice bluetoothDevice, int key)468 public static Uri getUriMetaData(BluetoothDevice bluetoothDevice, int key) { 469 String data = getStringMetaData(bluetoothDevice, key); 470 if (data == null) { 471 return null; 472 } 473 return Uri.parse(data); 474 } 475 476 /** 477 * Get URI Bluetooth metadata for extra control 478 * 479 * @param bluetoothDevice the BluetoothDevice to get metadata 480 * @return the URI metadata 481 */ getControlUriMetaData(BluetoothDevice bluetoothDevice)482 public static String getControlUriMetaData(BluetoothDevice bluetoothDevice) { 483 String data = getStringMetaData(bluetoothDevice, METADATA_FAST_PAIR_CUSTOMIZED_FIELDS); 484 return extraTagValue(KEY_HEARABLE_CONTROL_SLICE, data); 485 } 486 487 /** 488 * Check if the Bluetooth device is an AvailableMediaBluetoothDevice, which means: 1) currently 489 * connected 2) is Hearing Aid or LE Audio OR 3) connected profile matches currentAudioProfile 490 * 491 * @param cachedDevice the CachedBluetoothDevice 492 * @param audioManager audio manager to get the current audio profile 493 * @return if the device is AvailableMediaBluetoothDevice 494 */ 495 @WorkerThread isAvailableMediaBluetoothDevice( CachedBluetoothDevice cachedDevice, AudioManager audioManager)496 public static boolean isAvailableMediaBluetoothDevice( 497 CachedBluetoothDevice cachedDevice, AudioManager audioManager) { 498 int audioMode = audioManager.getMode(); 499 int currentAudioProfile; 500 501 if (audioMode == AudioManager.MODE_RINGTONE 502 || audioMode == AudioManager.MODE_IN_CALL 503 || audioMode == AudioManager.MODE_IN_COMMUNICATION) { 504 // in phone call 505 currentAudioProfile = BluetoothProfile.HEADSET; 506 } else { 507 // without phone call 508 currentAudioProfile = BluetoothProfile.A2DP; 509 } 510 511 boolean isFilterMatched = false; 512 if (isDeviceConnected(cachedDevice)) { 513 // If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP. 514 // It would show in Available Devices group. 515 if (cachedDevice.isConnectedAshaHearingAidDevice() 516 || cachedDevice.isConnectedLeAudioDevice()) { 517 Log.d( 518 TAG, 519 "isFilterMatched() device : " 520 + cachedDevice.getName() 521 + ", the profile is connected."); 522 return true; 523 } 524 // According to the current audio profile type, 525 // this page will show the bluetooth device that have corresponding profile. 526 // For example: 527 // If current audio profile is a2dp, show the bluetooth device that have a2dp profile. 528 // If current audio profile is headset, 529 // show the bluetooth device that have headset profile. 530 switch (currentAudioProfile) { 531 case BluetoothProfile.A2DP: 532 isFilterMatched = cachedDevice.isConnectedA2dpDevice(); 533 break; 534 case BluetoothProfile.HEADSET: 535 isFilterMatched = cachedDevice.isConnectedHfpDevice(); 536 break; 537 } 538 } 539 return isFilterMatched; 540 } 541 542 /** Returns if the le audio sharing is enabled. */ isAudioSharingEnabled()543 public static boolean isAudioSharingEnabled() { 544 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 545 try { 546 return Flags.enableLeAudioSharing() 547 && adapter.isLeAudioBroadcastSourceSupported() 548 == BluetoothStatusCodes.FEATURE_SUPPORTED 549 && adapter.isLeAudioBroadcastAssistantSupported() 550 == BluetoothStatusCodes.FEATURE_SUPPORTED; 551 } catch (IllegalStateException e) { 552 Log.d(TAG, "LE state is on, but there is no bluetooth service.", e); 553 return false; 554 } 555 } 556 557 /** Returns if the broadcast is on-going. */ 558 @WorkerThread isBroadcasting(@ullable LocalBluetoothManager manager)559 public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) { 560 if (manager == null) return false; 561 LocalBluetoothLeBroadcast broadcast = 562 manager.getProfileManager().getLeAudioBroadcastProfile(); 563 return broadcast != null && broadcast.isEnabled(null); 564 } 565 566 /** 567 * Check if {@link CachedBluetoothDevice} has connected to a broadcast source. 568 * 569 * @param cachedDevice The cached bluetooth device to check. 570 * @param localBtManager The BT manager to provide BT functions. 571 * @return Whether the device has connected to a broadcast source. 572 */ 573 @WorkerThread hasConnectedBroadcastSource( CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager)574 public static boolean hasConnectedBroadcastSource( 575 CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) { 576 if (localBtManager == null) { 577 Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null"); 578 return false; 579 } 580 LocalBluetoothLeBroadcastAssistant assistant = 581 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 582 if (assistant == null) { 583 Log.d(TAG, "Skip check hasConnectedBroadcastSource due to assistant profile is null"); 584 return false; 585 } 586 List<BluetoothLeBroadcastReceiveState> sourceList = 587 assistant.getAllSources(cachedDevice.getDevice()); 588 if (!sourceList.isEmpty() && sourceList.stream().anyMatch(BluetoothUtils::isConnected)) { 589 Log.d( 590 TAG, 591 "Lead device has connected broadcast source, device = " 592 + cachedDevice.getDevice().getAnonymizedAddress()); 593 return true; 594 } 595 // Return true if member device is in broadcast. 596 for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) { 597 List<BluetoothLeBroadcastReceiveState> list = 598 assistant.getAllSources(device.getDevice()); 599 if (!list.isEmpty() && list.stream().anyMatch(BluetoothUtils::isConnected)) { 600 Log.d( 601 TAG, 602 "Member device has connected broadcast source, device = " 603 + device.getDevice().getAnonymizedAddress()); 604 return true; 605 } 606 } 607 return false; 608 } 609 610 /** Checks the connectivity status based on the provided broadcast receive state. */ 611 @WorkerThread isConnected(BluetoothLeBroadcastReceiveState state)612 public static boolean isConnected(BluetoothLeBroadcastReceiveState state) { 613 return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0); 614 } 615 616 /** 617 * Checks if the Bluetooth device is an available hearing device, which means: 1) currently 618 * connected 2) is Hearing Aid 3) connected profile match hearing aid related profiles (e.g. 619 * ASHA, HAP) 620 * 621 * @param cachedDevice the CachedBluetoothDevice 622 * @return if the device is Available hearing device 623 */ 624 @WorkerThread isAvailableHearingDevice(CachedBluetoothDevice cachedDevice)625 public static boolean isAvailableHearingDevice(CachedBluetoothDevice cachedDevice) { 626 if (isDeviceConnected(cachedDevice) && cachedDevice.isConnectedHearingAidDevice()) { 627 Log.d( 628 TAG, 629 "isFilterMatched() device : " 630 + cachedDevice.getName() 631 + ", the profile is connected."); 632 return true; 633 } 634 return false; 635 } 636 637 /** 638 * Check if the Bluetooth device is a ConnectedBluetoothDevice, which means: 1) currently 639 * connected 2) is not Hearing Aid or LE Audio AND 3) connected profile does not match 640 * currentAudioProfile 641 * 642 * @param cachedDevice the CachedBluetoothDevice 643 * @param audioManager audio manager to get the current audio profile 644 * @return if the device is AvailableMediaBluetoothDevice 645 */ 646 @WorkerThread isConnectedBluetoothDevice( CachedBluetoothDevice cachedDevice, AudioManager audioManager)647 public static boolean isConnectedBluetoothDevice( 648 CachedBluetoothDevice cachedDevice, AudioManager audioManager) { 649 int audioMode = audioManager.getMode(); 650 int currentAudioProfile; 651 652 if (audioMode == AudioManager.MODE_RINGTONE 653 || audioMode == AudioManager.MODE_IN_CALL 654 || audioMode == AudioManager.MODE_IN_COMMUNICATION) { 655 // in phone call 656 currentAudioProfile = BluetoothProfile.HEADSET; 657 } else { 658 // without phone call 659 currentAudioProfile = BluetoothProfile.A2DP; 660 } 661 662 boolean isFilterMatched = false; 663 if (isDeviceConnected(cachedDevice)) { 664 // If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP. 665 // It would not show in Connected Devices group. 666 if (cachedDevice.isConnectedAshaHearingAidDevice() 667 || cachedDevice.isConnectedLeAudioDevice()) { 668 return false; 669 } 670 // According to the current audio profile type, 671 // this page will show the bluetooth device that doesn't have corresponding profile. 672 // For example: 673 // If current audio profile is a2dp, 674 // show the bluetooth device that doesn't have a2dp profile. 675 // If current audio profile is headset, 676 // show the bluetooth device that doesn't have headset profile. 677 switch (currentAudioProfile) { 678 case BluetoothProfile.A2DP: 679 isFilterMatched = !cachedDevice.isConnectedA2dpDevice(); 680 break; 681 case BluetoothProfile.HEADSET: 682 isFilterMatched = !cachedDevice.isConnectedHfpDevice(); 683 break; 684 } 685 } 686 return isFilterMatched; 687 } 688 689 /** 690 * Check if the Bluetooth device is an active media device 691 * 692 * @param cachedDevice the CachedBluetoothDevice 693 * @return if the Bluetooth device is an active media device 694 */ isActiveMediaDevice(CachedBluetoothDevice cachedDevice)695 public static boolean isActiveMediaDevice(CachedBluetoothDevice cachedDevice) { 696 return cachedDevice.isActiveDevice(BluetoothProfile.A2DP) 697 || cachedDevice.isActiveDevice(BluetoothProfile.HEADSET) 698 || cachedDevice.isActiveDevice(BluetoothProfile.HEARING_AID) 699 || cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO); 700 } 701 702 /** 703 * Check if the Bluetooth device is an active LE Audio device 704 * 705 * @param cachedDevice the CachedBluetoothDevice 706 * @return if the Bluetooth device is an active LE Audio device 707 */ isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice)708 public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) { 709 return cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO); 710 } 711 isDeviceConnected(CachedBluetoothDevice cachedDevice)712 private static boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) { 713 if (cachedDevice == null) { 714 return false; 715 } 716 final BluetoothDevice device = cachedDevice.getDevice(); 717 return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected(); 718 } 719 720 @SuppressLint("NewApi") // Hidden API made public doesClassMatch(BluetoothClass btClass, int classId)721 private static boolean doesClassMatch(BluetoothClass btClass, int classId) { 722 return btClass.doesClassMatch(classId); 723 } 724 extraTagValue(String tag, String metaData)725 private static String extraTagValue(String tag, String metaData) { 726 if (TextUtils.isEmpty(metaData)) { 727 return null; 728 } 729 Pattern pattern = Pattern.compile(generateExpressionWithTag(tag, "(.*?)")); 730 Matcher matcher = pattern.matcher(metaData); 731 if (matcher.find()) { 732 return matcher.group(1); 733 } 734 return null; 735 } 736 getTagStart(String tag)737 private static String getTagStart(String tag) { 738 return String.format(Locale.ENGLISH, "<%s>", tag); 739 } 740 getTagEnd(String tag)741 private static String getTagEnd(String tag) { 742 return String.format(Locale.ENGLISH, "</%s>", tag); 743 } 744 generateExpressionWithTag(String tag, String value)745 private static String generateExpressionWithTag(String tag, String value) { 746 return getTagStart(tag) + value + getTagEnd(tag); 747 } 748 749 /** 750 * Returns the BluetoothDevice's exclusive manager ({@link 751 * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists, otherwise null. 752 */ 753 @Nullable getExclusiveManager(BluetoothDevice bluetoothDevice)754 private static String getExclusiveManager(BluetoothDevice bluetoothDevice) { 755 byte[] exclusiveManagerBytes = 756 bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER); 757 if (exclusiveManagerBytes == null) { 758 Log.d( 759 TAG, 760 "Bluetooth device " 761 + bluetoothDevice.getName() 762 + " doesn't have exclusive manager"); 763 return null; 764 } 765 return new String(exclusiveManagerBytes); 766 } 767 768 /** Checks if given package is installed and enabled */ isPackageInstalledAndEnabled(Context context, String packageName)769 private static boolean isPackageInstalledAndEnabled(Context context, String packageName) { 770 PackageManager packageManager = context.getPackageManager(); 771 try { 772 ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0); 773 return appInfo.enabled; 774 } catch (PackageManager.NameNotFoundException e) { 775 Log.d(TAG, "Package " + packageName + " is not installed/enabled"); 776 } 777 return false; 778 } 779 780 /** 781 * A BluetoothDevice is exclusively managed if 1) it has field {@link 782 * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata. 2) the exclusive manager app is 783 * installed and enabled. 784 */ isExclusivelyManagedBluetoothDevice( @onNull Context context, @NonNull BluetoothDevice bluetoothDevice)785 public static boolean isExclusivelyManagedBluetoothDevice( 786 @NonNull Context context, @NonNull BluetoothDevice bluetoothDevice) { 787 String exclusiveManagerName = getExclusiveManager(bluetoothDevice); 788 if (exclusiveManagerName == null) { 789 return false; 790 } 791 792 ComponentName exclusiveManagerComponent = 793 ComponentName.unflattenFromString(exclusiveManagerName); 794 String exclusiveManagerPackage = exclusiveManagerComponent != null 795 ? exclusiveManagerComponent.getPackageName() : exclusiveManagerName; 796 797 if (!isPackageInstalledAndEnabled(context, exclusiveManagerPackage)) { 798 return false; 799 } else { 800 Log.d(TAG, "Found exclusively managed app " + exclusiveManagerPackage); 801 return true; 802 } 803 } 804 805 /** 806 * Get CSIP group id for {@link CachedBluetoothDevice}. 807 * 808 * <p>If CachedBluetoothDevice#getGroupId is invalid, fetch group id from 809 * LeAudioProfile#getGroupId. 810 */ getGroupId(@onNull CachedBluetoothDevice cachedDevice)811 public static int getGroupId(@NonNull CachedBluetoothDevice cachedDevice) { 812 int groupId = cachedDevice.getGroupId(); 813 String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress(); 814 if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 815 Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress); 816 return groupId; 817 } 818 for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) { 819 if (profile instanceof LeAudioProfile) { 820 Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress); 821 return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice()); 822 } 823 } 824 Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress); 825 return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 826 } 827 } 828