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