1 package com.android.settingslib.bluetooth;
2 
3 import android.bluetooth.BluetoothClass;
4 import android.bluetooth.BluetoothDevice;
5 import android.bluetooth.BluetoothProfile;
6 import android.content.Context;
7 import android.content.Intent;
8 import android.content.res.Resources;
9 import android.graphics.Bitmap;
10 import android.graphics.drawable.Drawable;
11 import android.net.Uri;
12 import android.provider.MediaStore;
13 import android.util.Log;
14 import android.util.Pair;
15 
16 import androidx.annotation.DrawableRes;
17 
18 import com.android.settingslib.R;
19 import com.android.settingslib.widget.AdaptiveIcon;
20 import com.android.settingslib.widget.AdaptiveOutlineDrawable;
21 
22 import java.io.IOException;
23 import java.util.List;
24 
25 public class BluetoothUtils {
26     private static final String TAG = "BluetoothUtils";
27 
28     public static final boolean V = false; // verbose logging
29     public static final boolean D = true;  // regular logging
30 
31     public static final int META_INT_ERROR = -1;
32 
33     private static ErrorListener sErrorListener;
34 
getConnectionStateSummary(int connectionState)35     public static int getConnectionStateSummary(int connectionState) {
36         switch (connectionState) {
37             case BluetoothProfile.STATE_CONNECTED:
38                 return R.string.bluetooth_connected;
39             case BluetoothProfile.STATE_CONNECTING:
40                 return R.string.bluetooth_connecting;
41             case BluetoothProfile.STATE_DISCONNECTED:
42                 return R.string.bluetooth_disconnected;
43             case BluetoothProfile.STATE_DISCONNECTING:
44                 return R.string.bluetooth_disconnecting;
45             default:
46                 return 0;
47         }
48     }
49 
showError(Context context, String name, int messageResId)50     static void showError(Context context, String name, int messageResId) {
51         if (sErrorListener != null) {
52             sErrorListener.onShowError(context, name, messageResId);
53         }
54     }
55 
setErrorListener(ErrorListener listener)56     public static void setErrorListener(ErrorListener listener) {
57         sErrorListener = listener;
58     }
59 
60     public interface ErrorListener {
onShowError(Context context, String name, int messageResId)61         void onShowError(Context context, String name, int messageResId);
62     }
63 
getBtClassDrawableWithDescription(Context context, CachedBluetoothDevice cachedDevice)64     public static Pair<Drawable, String> getBtClassDrawableWithDescription(Context context,
65             CachedBluetoothDevice cachedDevice) {
66         BluetoothClass btClass = cachedDevice.getBtClass();
67         if (btClass != null) {
68             switch (btClass.getMajorDeviceClass()) {
69                 case BluetoothClass.Device.Major.COMPUTER:
70                     return new Pair<>(getBluetoothDrawable(context,
71                             com.android.internal.R.drawable.ic_bt_laptop),
72                             context.getString(R.string.bluetooth_talkback_computer));
73 
74                 case BluetoothClass.Device.Major.PHONE:
75                     return new Pair<>(
76                             getBluetoothDrawable(context,
77                                     com.android.internal.R.drawable.ic_phone),
78                             context.getString(R.string.bluetooth_talkback_phone));
79 
80                 case BluetoothClass.Device.Major.PERIPHERAL:
81                     return new Pair<>(
82                             getBluetoothDrawable(context, HidProfile.getHidClassDrawable(btClass)),
83                             context.getString(R.string.bluetooth_talkback_input_peripheral));
84 
85                 case BluetoothClass.Device.Major.IMAGING:
86                     return new Pair<>(
87                             getBluetoothDrawable(context,
88                                     com.android.internal.R.drawable.ic_settings_print),
89                             context.getString(R.string.bluetooth_talkback_imaging));
90 
91                 default:
92                     // unrecognized device class; continue
93             }
94         }
95 
96         List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles();
97         for (LocalBluetoothProfile profile : profiles) {
98             int resId = profile.getDrawableResource(btClass);
99             if (resId != 0) {
100                 return new Pair<>(getBluetoothDrawable(context, resId), null);
101             }
102         }
103         if (btClass != null) {
104             if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
105                 return new Pair<>(
106                         getBluetoothDrawable(context,
107                                 com.android.internal.R.drawable.ic_bt_headset_hfp),
108                         context.getString(R.string.bluetooth_talkback_headset));
109             }
110             if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
111                 return new Pair<>(
112                         getBluetoothDrawable(context,
113                                 com.android.internal.R.drawable.ic_bt_headphones_a2dp),
114                         context.getString(R.string.bluetooth_talkback_headphone));
115             }
116         }
117         return new Pair<>(
118                 getBluetoothDrawable(context,
119                         com.android.internal.R.drawable.ic_settings_bluetooth).mutate(),
120                 context.getString(R.string.bluetooth_talkback_bluetooth));
121     }
122 
123     /**
124      * Get bluetooth drawable by {@code resId}
125      */
getBluetoothDrawable(Context context, @DrawableRes int resId)126     public static Drawable getBluetoothDrawable(Context context, @DrawableRes int resId) {
127         return context.getDrawable(resId);
128     }
129 
130     /**
131      * Get colorful bluetooth icon with description
132      */
getBtRainbowDrawableWithDescription(Context context, CachedBluetoothDevice cachedDevice)133     public static Pair<Drawable, String> getBtRainbowDrawableWithDescription(Context context,
134             CachedBluetoothDevice cachedDevice) {
135         final Pair<Drawable, String> pair = BluetoothUtils.getBtClassDrawableWithDescription(
136                 context, cachedDevice);
137         final BluetoothDevice bluetoothDevice = cachedDevice.getDevice();
138         final boolean untetheredHeadset = getBooleanMetaData(
139                 bluetoothDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET);
140         final int iconSize = context.getResources().getDimensionPixelSize(
141                 R.dimen.bt_nearby_icon_size);
142         final Resources resources = context.getResources();
143 
144         // Deal with untethered headset
145         if (untetheredHeadset) {
146             final Uri iconUri = getUriMetaData(bluetoothDevice,
147                     BluetoothDevice.METADATA_MAIN_ICON);
148             if (iconUri != null) {
149                 try {
150                     context.getContentResolver().takePersistableUriPermission(iconUri,
151                             Intent.FLAG_GRANT_READ_URI_PERMISSION);
152                 } catch (SecurityException e) {
153                     Log.e(TAG, "Failed to take persistable permission for: " + iconUri);
154                 }
155                 try {
156                     final Bitmap bitmap = MediaStore.Images.Media.getBitmap(
157                             context.getContentResolver(), iconUri);
158                     if (bitmap != null) {
159                         final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize,
160                                 iconSize, false);
161                         bitmap.recycle();
162                         final AdaptiveOutlineDrawable drawable = new AdaptiveOutlineDrawable(
163                                 resources, resizedBitmap);
164                         return new Pair<>(drawable, pair.second);
165                     }
166                 } catch (IOException e) {
167                     Log.e(TAG, "Failed to get drawable for: " + iconUri, e);
168                 }
169             }
170         }
171 
172         return new Pair<>(buildBtRainbowDrawable(context,
173                 pair.first, cachedDevice.getAddress().hashCode()), pair.second);
174     }
175 
176     /**
177      * Build Bluetooth device icon with rainbow
178      */
buildBtRainbowDrawable(Context context, Drawable drawable, int hashCode)179     public static Drawable buildBtRainbowDrawable(Context context, Drawable drawable,
180             int hashCode) {
181         final Resources resources = context.getResources();
182 
183         // Deal with normal headset
184         final int[] iconFgColors = resources.getIntArray(R.array.bt_icon_fg_colors);
185         final int[] iconBgColors = resources.getIntArray(R.array.bt_icon_bg_colors);
186 
187         // get color index based on mac address
188         final int index = Math.abs(hashCode % iconBgColors.length);
189         drawable.setTint(iconFgColors[index]);
190         final Drawable adaptiveIcon = new AdaptiveIcon(context, drawable);
191         ((AdaptiveIcon) adaptiveIcon).setBackgroundColor(iconBgColors[index]);
192 
193         return adaptiveIcon;
194     }
195 
196     /**
197      * Get boolean Bluetooth metadata
198      *
199      * @param bluetoothDevice the BluetoothDevice to get metadata
200      * @param key key value within the list of BluetoothDevice.METADATA_*
201      * @return the boolean metdata
202      */
getBooleanMetaData(BluetoothDevice bluetoothDevice, int key)203     public static boolean getBooleanMetaData(BluetoothDevice bluetoothDevice, int key) {
204         if (bluetoothDevice == null) {
205             return false;
206         }
207         final byte[] data = bluetoothDevice.getMetadata(key);
208         if (data == null) {
209             return false;
210         }
211         return Boolean.parseBoolean(new String(data));
212     }
213 
214     /**
215      * Get String Bluetooth metadata
216      *
217      * @param bluetoothDevice the BluetoothDevice to get metadata
218      * @param key key value within the list of BluetoothDevice.METADATA_*
219      * @return the String metdata
220      */
getStringMetaData(BluetoothDevice bluetoothDevice, int key)221     public static String getStringMetaData(BluetoothDevice bluetoothDevice, int key) {
222         if (bluetoothDevice == null) {
223             return null;
224         }
225         final byte[] data = bluetoothDevice.getMetadata(key);
226         if (data == null) {
227             return null;
228         }
229         return new String(data);
230     }
231 
232     /**
233      * Get integer Bluetooth metadata
234      *
235      * @param bluetoothDevice the BluetoothDevice to get metadata
236      * @param key key value within the list of BluetoothDevice.METADATA_*
237      * @return the int metdata
238      */
getIntMetaData(BluetoothDevice bluetoothDevice, int key)239     public static int getIntMetaData(BluetoothDevice bluetoothDevice, int key) {
240         if (bluetoothDevice == null) {
241             return META_INT_ERROR;
242         }
243         final byte[] data = bluetoothDevice.getMetadata(key);
244         if (data == null) {
245             return META_INT_ERROR;
246         }
247         try {
248             return Integer.parseInt(new String(data));
249         } catch (NumberFormatException e) {
250             return META_INT_ERROR;
251         }
252     }
253 
254     /**
255      * Get URI Bluetooth metadata
256      *
257      * @param bluetoothDevice the BluetoothDevice to get metadata
258      * @param key key value within the list of BluetoothDevice.METADATA_*
259      * @return the URI metdata
260      */
getUriMetaData(BluetoothDevice bluetoothDevice, int key)261     public static Uri getUriMetaData(BluetoothDevice bluetoothDevice, int key) {
262         String data = getStringMetaData(bluetoothDevice, key);
263         if (data == null) {
264             return null;
265         }
266         return Uri.parse(data);
267     }
268 }
269