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