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.BluetoothClass; 7 import android.bluetooth.BluetoothDevice; 8 import android.bluetooth.BluetoothProfile; 9 import android.bluetooth.BluetoothCsipSetCoordinator; 10 import android.content.Context; 11 import android.content.Intent; 12 import android.content.res.Resources; 13 import android.graphics.Bitmap; 14 import android.graphics.Canvas; 15 import android.graphics.drawable.BitmapDrawable; 16 import android.graphics.drawable.Drawable; 17 import android.net.Uri; 18 import android.provider.DeviceConfig; 19 import android.provider.MediaStore; 20 import android.text.TextUtils; 21 import android.util.Log; 22 import android.util.Pair; 23 24 import androidx.annotation.DrawableRes; 25 import androidx.annotation.NonNull; 26 import androidx.core.graphics.drawable.IconCompat; 27 28 import com.android.settingslib.R; 29 import com.android.settingslib.widget.AdaptiveIcon; 30 import com.android.settingslib.widget.AdaptiveOutlineDrawable; 31 32 import java.io.IOException; 33 import java.util.List; 34 import java.util.Locale; 35 import java.util.regex.Matcher; 36 import java.util.regex.Pattern; 37 38 public class BluetoothUtils { 39 private static final String TAG = "BluetoothUtils"; 40 41 public static final boolean V = false; // verbose logging 42 public static final boolean D = true; // regular logging 43 44 public static final int META_INT_ERROR = -1; 45 public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled"; 46 private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; 47 private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH"; 48 49 private static ErrorListener sErrorListener; 50 getConnectionStateSummary(int connectionState)51 public static int getConnectionStateSummary(int connectionState) { 52 switch (connectionState) { 53 case BluetoothProfile.STATE_CONNECTED: 54 return R.string.bluetooth_connected; 55 case BluetoothProfile.STATE_CONNECTING: 56 return R.string.bluetooth_connecting; 57 case BluetoothProfile.STATE_DISCONNECTED: 58 return R.string.bluetooth_disconnected; 59 case BluetoothProfile.STATE_DISCONNECTING: 60 return R.string.bluetooth_disconnecting; 61 default: 62 return 0; 63 } 64 } 65 showError(Context context, String name, int messageResId)66 static void showError(Context context, String name, int messageResId) { 67 if (sErrorListener != null) { 68 sErrorListener.onShowError(context, name, messageResId); 69 } 70 } 71 setErrorListener(ErrorListener listener)72 public static void setErrorListener(ErrorListener listener) { 73 sErrorListener = listener; 74 } 75 76 public interface ErrorListener { onShowError(Context context, String name, int messageResId)77 void onShowError(Context context, String name, int messageResId); 78 } 79 80 /** 81 * @param context to access resources from 82 * @param cachedDevice to get class from 83 * @return pair containing the drawable and the description of the Bluetooth class 84 * of the device. 85 */ getBtClassDrawableWithDescription(Context context, CachedBluetoothDevice cachedDevice)86 public static Pair<Drawable, String> getBtClassDrawableWithDescription(Context context, 87 CachedBluetoothDevice cachedDevice) { 88 BluetoothClass btClass = cachedDevice.getBtClass(); 89 if (btClass != null) { 90 switch (btClass.getMajorDeviceClass()) { 91 case BluetoothClass.Device.Major.COMPUTER: 92 return new Pair<>(getBluetoothDrawable(context, 93 com.android.internal.R.drawable.ic_bt_laptop), 94 context.getString(R.string.bluetooth_talkback_computer)); 95 96 case BluetoothClass.Device.Major.PHONE: 97 return new Pair<>( 98 getBluetoothDrawable(context, 99 com.android.internal.R.drawable.ic_phone), 100 context.getString(R.string.bluetooth_talkback_phone)); 101 102 case BluetoothClass.Device.Major.PERIPHERAL: 103 return new Pair<>( 104 getBluetoothDrawable(context, HidProfile.getHidClassDrawable(btClass)), 105 context.getString(R.string.bluetooth_talkback_input_peripheral)); 106 107 case BluetoothClass.Device.Major.IMAGING: 108 return new Pair<>( 109 getBluetoothDrawable(context, 110 com.android.internal.R.drawable.ic_settings_print), 111 context.getString(R.string.bluetooth_talkback_imaging)); 112 113 default: 114 // unrecognized device class; continue 115 } 116 } 117 118 List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles(); 119 int resId = 0; 120 for (LocalBluetoothProfile profile : profiles) { 121 int profileResId = profile.getDrawableResource(btClass); 122 if (profileResId != 0) { 123 // The device should show hearing aid icon if it contains any hearing aid related 124 // profiles 125 if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) { 126 return new Pair<>(getBluetoothDrawable(context, profileResId), null); 127 } 128 if (resId == 0) { 129 resId = profileResId; 130 } 131 } 132 } 133 if (resId != 0) { 134 return new Pair<>(getBluetoothDrawable(context, resId), null); 135 } 136 137 if (btClass != null) { 138 if (doesClassMatch(btClass, BluetoothClass.PROFILE_HEADSET)) { 139 return new Pair<>( 140 getBluetoothDrawable(context, 141 com.android.internal.R.drawable.ic_bt_headset_hfp), 142 context.getString(R.string.bluetooth_talkback_headset)); 143 } 144 if (doesClassMatch(btClass, BluetoothClass.PROFILE_A2DP)) { 145 return new Pair<>( 146 getBluetoothDrawable(context, 147 com.android.internal.R.drawable.ic_bt_headphones_a2dp), 148 context.getString(R.string.bluetooth_talkback_headphone)); 149 } 150 } 151 return new Pair<>( 152 getBluetoothDrawable(context, 153 com.android.internal.R.drawable.ic_settings_bluetooth).mutate(), 154 context.getString(R.string.bluetooth_talkback_bluetooth)); 155 } 156 157 /** 158 * Get bluetooth drawable by {@code resId} 159 */ getBluetoothDrawable(Context context, @DrawableRes int resId)160 public static Drawable getBluetoothDrawable(Context context, @DrawableRes int resId) { 161 return context.getDrawable(resId); 162 } 163 164 /** 165 * Get colorful bluetooth icon with description 166 */ getBtRainbowDrawableWithDescription(Context context, CachedBluetoothDevice cachedDevice)167 public static Pair<Drawable, String> getBtRainbowDrawableWithDescription(Context context, 168 CachedBluetoothDevice cachedDevice) { 169 final Resources resources = context.getResources(); 170 final Pair<Drawable, String> pair = BluetoothUtils.getBtDrawableWithDescription(context, 171 cachedDevice); 172 173 if (pair.first instanceof BitmapDrawable) { 174 return new Pair<>(new AdaptiveOutlineDrawable( 175 resources, ((BitmapDrawable) pair.first).getBitmap()), pair.second); 176 } 177 178 int hashCode; 179 if ((cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)) { 180 hashCode = new Integer(cachedDevice.getGroupId()).hashCode(); 181 } else { 182 hashCode = cachedDevice.getAddress().hashCode(); 183 } 184 185 return new Pair<>(buildBtRainbowDrawable(context, 186 pair.first, hashCode), pair.second); 187 } 188 189 /** 190 * Build Bluetooth device icon with rainbow 191 */ buildBtRainbowDrawable(Context context, Drawable drawable, int hashCode)192 private static Drawable buildBtRainbowDrawable(Context context, Drawable drawable, 193 int hashCode) { 194 final Resources resources = context.getResources(); 195 196 // Deal with normal headset 197 final int[] iconFgColors = resources.getIntArray(R.array.bt_icon_fg_colors); 198 final int[] iconBgColors = resources.getIntArray(R.array.bt_icon_bg_colors); 199 200 // get color index based on mac address 201 final int index = Math.abs(hashCode % iconBgColors.length); 202 drawable.setTint(iconFgColors[index]); 203 final Drawable adaptiveIcon = new AdaptiveIcon(context, drawable); 204 ((AdaptiveIcon) adaptiveIcon).setBackgroundColor(iconBgColors[index]); 205 206 return adaptiveIcon; 207 } 208 209 /** 210 * Get bluetooth icon with description 211 */ getBtDrawableWithDescription(Context context, CachedBluetoothDevice cachedDevice)212 public static Pair<Drawable, String> getBtDrawableWithDescription(Context context, 213 CachedBluetoothDevice cachedDevice) { 214 final Pair<Drawable, String> pair = BluetoothUtils.getBtClassDrawableWithDescription( 215 context, cachedDevice); 216 final BluetoothDevice bluetoothDevice = cachedDevice.getDevice(); 217 final int iconSize = context.getResources().getDimensionPixelSize( 218 R.dimen.bt_nearby_icon_size); 219 final Resources resources = context.getResources(); 220 221 // Deal with advanced device icon 222 if (isAdvancedDetailsHeader(bluetoothDevice)) { 223 final Uri iconUri = getUriMetaData(bluetoothDevice, 224 BluetoothDevice.METADATA_MAIN_ICON); 225 if (iconUri != null) { 226 try { 227 context.getContentResolver().takePersistableUriPermission(iconUri, 228 Intent.FLAG_GRANT_READ_URI_PERMISSION); 229 } catch (SecurityException e) { 230 Log.e(TAG, "Failed to take persistable permission for: " + iconUri, e); 231 } 232 try { 233 final Bitmap bitmap = MediaStore.Images.Media.getBitmap( 234 context.getContentResolver(), iconUri); 235 if (bitmap != null) { 236 final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize, 237 iconSize, false); 238 bitmap.recycle(); 239 return new Pair<>(new BitmapDrawable(resources, 240 resizedBitmap), pair.second); 241 } 242 } catch (IOException e) { 243 Log.e(TAG, "Failed to get drawable for: " + iconUri, e); 244 } catch (SecurityException e) { 245 Log.e(TAG, "Failed to get permission for: " + iconUri, e); 246 } 247 } 248 } 249 250 return new Pair<>(pair.first, pair.second); 251 } 252 253 /** 254 * Check if the Bluetooth device supports advanced metadata 255 * 256 * @param bluetoothDevice the BluetoothDevice to get metadata 257 * @return true if it supports advanced metadata, false otherwise. 258 */ isAdvancedDetailsHeader(@onNull BluetoothDevice bluetoothDevice)259 public static boolean isAdvancedDetailsHeader(@NonNull BluetoothDevice bluetoothDevice) { 260 if (!isAdvancedHeaderEnabled()) { 261 return false; 262 } 263 if (isUntetheredHeadset(bluetoothDevice)) { 264 return true; 265 } 266 // The metadata is for Android S 267 String deviceType = getStringMetaData(bluetoothDevice, 268 BluetoothDevice.METADATA_DEVICE_TYPE); 269 if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET) 270 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_WATCH) 271 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_DEFAULT) 272 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_STYLUS)) { 273 Log.d(TAG, "isAdvancedDetailsHeader: deviceType is " + deviceType); 274 return true; 275 } 276 return false; 277 } 278 279 /** 280 * Check if the Bluetooth device is supports advanced metadata and an untethered headset 281 * 282 * @param bluetoothDevice the BluetoothDevice to get metadata 283 * @return true if it supports advanced metadata and an untethered headset, false otherwise. 284 */ isAdvancedUntetheredDevice(@onNull BluetoothDevice bluetoothDevice)285 public static boolean isAdvancedUntetheredDevice(@NonNull BluetoothDevice bluetoothDevice) { 286 if (!isAdvancedHeaderEnabled()) { 287 return false; 288 } 289 if (isUntetheredHeadset(bluetoothDevice)) { 290 return true; 291 } 292 // The metadata is for Android S 293 String deviceType = getStringMetaData(bluetoothDevice, 294 BluetoothDevice.METADATA_DEVICE_TYPE); 295 if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) { 296 Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device "); 297 return true; 298 } 299 return false; 300 } 301 302 /** 303 * Check if a device class matches with a defined BluetoothClass device. 304 * 305 * @param device Must be one of the public constants in {@link BluetoothClass.Device} 306 * @return true if device class matches, false otherwise. 307 */ isDeviceClassMatched(@onNull BluetoothDevice bluetoothDevice, int device)308 public static boolean isDeviceClassMatched(@NonNull BluetoothDevice bluetoothDevice, 309 int device) { 310 return bluetoothDevice.getBluetoothClass() != null 311 && bluetoothDevice.getBluetoothClass().getDeviceClass() == device; 312 } 313 isAdvancedHeaderEnabled()314 private static boolean isAdvancedHeaderEnabled() { 315 if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED, 316 true)) { 317 Log.d(TAG, "isAdvancedDetailsHeader: advancedEnabled is false"); 318 return false; 319 } 320 return true; 321 } 322 isUntetheredHeadset(@onNull BluetoothDevice bluetoothDevice)323 private static boolean isUntetheredHeadset(@NonNull BluetoothDevice bluetoothDevice) { 324 // The metadata is for Android R 325 if (getBooleanMetaData(bluetoothDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) { 326 Log.d(TAG, "isAdvancedDetailsHeader: untetheredHeadset is true"); 327 return true; 328 } 329 return false; 330 } 331 332 /** 333 * Create an Icon pointing to a drawable. 334 */ createIconWithDrawable(Drawable drawable)335 public static IconCompat createIconWithDrawable(Drawable drawable) { 336 Bitmap bitmap; 337 if (drawable instanceof BitmapDrawable) { 338 bitmap = ((BitmapDrawable) drawable).getBitmap(); 339 } else { 340 final int width = drawable.getIntrinsicWidth(); 341 final int height = drawable.getIntrinsicHeight(); 342 bitmap = createBitmap(drawable, 343 width > 0 ? width : 1, 344 height > 0 ? height : 1); 345 } 346 return IconCompat.createWithBitmap(bitmap); 347 } 348 349 /** 350 * Build device icon with advanced outline 351 */ buildAdvancedDrawable(Context context, Drawable drawable)352 public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) { 353 final int iconSize = context.getResources().getDimensionPixelSize( 354 R.dimen.advanced_icon_size); 355 final Resources resources = context.getResources(); 356 357 Bitmap bitmap = null; 358 if (drawable instanceof BitmapDrawable) { 359 bitmap = ((BitmapDrawable) drawable).getBitmap(); 360 } else { 361 final int width = drawable.getIntrinsicWidth(); 362 final int height = drawable.getIntrinsicHeight(); 363 bitmap = createBitmap(drawable, 364 width > 0 ? width : 1, 365 height > 0 ? height : 1); 366 } 367 368 if (bitmap != null) { 369 final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize, 370 iconSize, false); 371 bitmap.recycle(); 372 return new AdaptiveOutlineDrawable(resources, resizedBitmap, ICON_TYPE_ADVANCED); 373 } 374 375 return drawable; 376 } 377 378 /** 379 * Creates a drawable with specified width and height. 380 */ createBitmap(Drawable drawable, int width, int height)381 public static Bitmap createBitmap(Drawable drawable, int width, int height) { 382 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 383 final Canvas canvas = new Canvas(bitmap); 384 drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 385 drawable.draw(canvas); 386 return bitmap; 387 } 388 389 /** 390 * Get boolean Bluetooth metadata 391 * 392 * @param bluetoothDevice the BluetoothDevice to get metadata 393 * @param key key value within the list of BluetoothDevice.METADATA_* 394 * @return the boolean metdata 395 */ getBooleanMetaData(BluetoothDevice bluetoothDevice, int key)396 public static boolean getBooleanMetaData(BluetoothDevice bluetoothDevice, int key) { 397 if (bluetoothDevice == null) { 398 return false; 399 } 400 final byte[] data = bluetoothDevice.getMetadata(key); 401 if (data == null) { 402 return false; 403 } 404 return Boolean.parseBoolean(new String(data)); 405 } 406 407 /** 408 * Get String Bluetooth metadata 409 * 410 * @param bluetoothDevice the BluetoothDevice to get metadata 411 * @param key key value within the list of BluetoothDevice.METADATA_* 412 * @return the String metdata 413 */ getStringMetaData(BluetoothDevice bluetoothDevice, int key)414 public static String getStringMetaData(BluetoothDevice bluetoothDevice, int key) { 415 if (bluetoothDevice == null) { 416 return null; 417 } 418 final byte[] data = bluetoothDevice.getMetadata(key); 419 if (data == null) { 420 return null; 421 } 422 return new String(data); 423 } 424 425 /** 426 * Get integer Bluetooth metadata 427 * 428 * @param bluetoothDevice the BluetoothDevice to get metadata 429 * @param key key value within the list of BluetoothDevice.METADATA_* 430 * @return the int metdata 431 */ getIntMetaData(BluetoothDevice bluetoothDevice, int key)432 public static int getIntMetaData(BluetoothDevice bluetoothDevice, int key) { 433 if (bluetoothDevice == null) { 434 return META_INT_ERROR; 435 } 436 final byte[] data = bluetoothDevice.getMetadata(key); 437 if (data == null) { 438 return META_INT_ERROR; 439 } 440 try { 441 return Integer.parseInt(new String(data)); 442 } catch (NumberFormatException e) { 443 return META_INT_ERROR; 444 } 445 } 446 447 /** 448 * Get URI Bluetooth metadata 449 * 450 * @param bluetoothDevice the BluetoothDevice to get metadata 451 * @param key key value within the list of BluetoothDevice.METADATA_* 452 * @return the URI metdata 453 */ getUriMetaData(BluetoothDevice bluetoothDevice, int key)454 public static Uri getUriMetaData(BluetoothDevice bluetoothDevice, int key) { 455 String data = getStringMetaData(bluetoothDevice, key); 456 if (data == null) { 457 return null; 458 } 459 return Uri.parse(data); 460 } 461 462 /** 463 * Get URI Bluetooth metadata for extra control 464 * 465 * @param bluetoothDevice the BluetoothDevice to get metadata 466 * @return the URI metadata 467 */ getControlUriMetaData(BluetoothDevice bluetoothDevice)468 public static String getControlUriMetaData(BluetoothDevice bluetoothDevice) { 469 String data = getStringMetaData(bluetoothDevice, METADATA_FAST_PAIR_CUSTOMIZED_FIELDS); 470 return extraTagValue(KEY_HEARABLE_CONTROL_SLICE, data); 471 } 472 473 @SuppressLint("NewApi") // Hidden API made public doesClassMatch(BluetoothClass btClass, int classId)474 private static boolean doesClassMatch(BluetoothClass btClass, int classId) { 475 return btClass.doesClassMatch(classId); 476 } 477 extraTagValue(String tag, String metaData)478 private static String extraTagValue(String tag, String metaData) { 479 if (TextUtils.isEmpty(metaData)) { 480 return null; 481 } 482 Pattern pattern = Pattern.compile(generateExpressionWithTag(tag, "(.*?)")); 483 Matcher matcher = pattern.matcher(metaData); 484 if (matcher.find()) { 485 return matcher.group(1); 486 } 487 return null; 488 } 489 getTagStart(String tag)490 private static String getTagStart(String tag) { 491 return String.format(Locale.ENGLISH, "<%s>", tag); 492 } 493 getTagEnd(String tag)494 private static String getTagEnd(String tag) { 495 return String.format(Locale.ENGLISH, "</%s>", tag); 496 } 497 generateExpressionWithTag(String tag, String value)498 private static String generateExpressionWithTag(String tag, String value) { 499 return getTagStart(tag) + value + getTagEnd(tag); 500 } 501 } 502