• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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