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