• 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.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