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