• 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.BluetoothAdapter;
7 import android.bluetooth.BluetoothClass;
8 import android.bluetooth.BluetoothCsipSetCoordinator;
9 import android.bluetooth.BluetoothDevice;
10 import android.bluetooth.BluetoothLeBroadcastReceiveState;
11 import android.bluetooth.BluetoothProfile;
12 import android.bluetooth.BluetoothStatusCodes;
13 import android.content.ComponentName;
14 import android.content.Context;
15 import android.content.Intent;
16 import android.content.pm.ApplicationInfo;
17 import android.content.pm.PackageManager;
18 import android.content.res.Resources;
19 import android.graphics.Bitmap;
20 import android.graphics.Canvas;
21 import android.graphics.drawable.BitmapDrawable;
22 import android.graphics.drawable.Drawable;
23 import android.media.AudioManager;
24 import android.net.Uri;
25 import android.provider.DeviceConfig;
26 import android.provider.MediaStore;
27 import android.text.TextUtils;
28 import android.util.Log;
29 import android.util.Pair;
30 
31 import androidx.annotation.DrawableRes;
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.annotation.WorkerThread;
35 import androidx.core.graphics.drawable.IconCompat;
36 
37 import com.android.settingslib.R;
38 import com.android.settingslib.flags.Flags;
39 import com.android.settingslib.widget.AdaptiveIcon;
40 import com.android.settingslib.widget.AdaptiveOutlineDrawable;
41 
42 import java.io.IOException;
43 import java.util.List;
44 import java.util.Locale;
45 import java.util.regex.Matcher;
46 import java.util.regex.Pattern;
47 
48 public class BluetoothUtils {
49     private static final String TAG = "BluetoothUtils";
50 
51     public static final boolean V = false; // verbose logging
52     public static final boolean D = true; // regular logging
53 
54     public static final int META_INT_ERROR = -1;
55     public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled";
56     private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
57     private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH";
58 
59     private static ErrorListener sErrorListener;
60 
getConnectionStateSummary(int connectionState)61     public static int getConnectionStateSummary(int connectionState) {
62         switch (connectionState) {
63             case BluetoothProfile.STATE_CONNECTED:
64                 return R.string.bluetooth_connected;
65             case BluetoothProfile.STATE_CONNECTING:
66                 return R.string.bluetooth_connecting;
67             case BluetoothProfile.STATE_DISCONNECTED:
68                 return R.string.bluetooth_disconnected;
69             case BluetoothProfile.STATE_DISCONNECTING:
70                 return R.string.bluetooth_disconnecting;
71             default:
72                 return 0;
73         }
74     }
75 
showError(Context context, String name, int messageResId)76     static void showError(Context context, String name, int messageResId) {
77         if (sErrorListener != null) {
78             sErrorListener.onShowError(context, name, messageResId);
79         }
80     }
81 
setErrorListener(ErrorListener listener)82     public static void setErrorListener(ErrorListener listener) {
83         sErrorListener = listener;
84     }
85 
86     public interface ErrorListener {
onShowError(Context context, String name, int messageResId)87         void onShowError(Context context, String name, int messageResId);
88     }
89 
90     /**
91      * @param context to access resources from
92      * @param cachedDevice to get class from
93      * @return pair containing the drawable and the description of the Bluetooth class of the
94      *     device.
95      */
getBtClassDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)96     public static Pair<Drawable, String> getBtClassDrawableWithDescription(
97             Context context, CachedBluetoothDevice cachedDevice) {
98         BluetoothClass btClass = cachedDevice.getBtClass();
99         if (btClass != null) {
100             switch (btClass.getMajorDeviceClass()) {
101                 case BluetoothClass.Device.Major.COMPUTER:
102                     return new Pair<>(
103                             getBluetoothDrawable(
104                                     context, com.android.internal.R.drawable.ic_bt_laptop),
105                             context.getString(R.string.bluetooth_talkback_computer));
106 
107                 case BluetoothClass.Device.Major.PHONE:
108                     return new Pair<>(
109                             getBluetoothDrawable(context, com.android.internal.R.drawable.ic_phone),
110                             context.getString(R.string.bluetooth_talkback_phone));
111 
112                 case BluetoothClass.Device.Major.PERIPHERAL:
113                     return new Pair<>(
114                             getBluetoothDrawable(context, HidProfile.getHidClassDrawable(btClass)),
115                             context.getString(R.string.bluetooth_talkback_input_peripheral));
116 
117                 case BluetoothClass.Device.Major.IMAGING:
118                     return new Pair<>(
119                             getBluetoothDrawable(
120                                     context, com.android.internal.R.drawable.ic_settings_print),
121                             context.getString(R.string.bluetooth_talkback_imaging));
122 
123                 default:
124                     // unrecognized device class; continue
125             }
126         }
127 
128         if (cachedDevice.isHearingAidDevice()) {
129             return new Pair<>(
130                     getBluetoothDrawable(
131                             context, com.android.internal.R.drawable.ic_bt_hearing_aid),
132                     context.getString(R.string.bluetooth_talkback_hearing_aids));
133         }
134 
135         List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles();
136         int resId = 0;
137         for (LocalBluetoothProfile profile : profiles) {
138             int profileResId = profile.getDrawableResource(btClass);
139             if (profileResId != 0) {
140                 // The device should show hearing aid icon if it contains any hearing aid related
141                 // profiles
142                 if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) {
143                     return new Pair<>(
144                             getBluetoothDrawable(context, profileResId),
145                             context.getString(R.string.bluetooth_talkback_hearing_aids));
146                 }
147                 if (resId == 0) {
148                     resId = profileResId;
149                 }
150             }
151         }
152         if (resId != 0) {
153             return new Pair<>(getBluetoothDrawable(context, resId), null);
154         }
155 
156         if (btClass != null) {
157             if (doesClassMatch(btClass, BluetoothClass.PROFILE_HEADSET)) {
158                 return new Pair<>(
159                         getBluetoothDrawable(
160                                 context, com.android.internal.R.drawable.ic_bt_headset_hfp),
161                         context.getString(R.string.bluetooth_talkback_headset));
162             }
163             if (doesClassMatch(btClass, BluetoothClass.PROFILE_A2DP)) {
164                 return new Pair<>(
165                         getBluetoothDrawable(
166                                 context, com.android.internal.R.drawable.ic_bt_headphones_a2dp),
167                         context.getString(R.string.bluetooth_talkback_headphone));
168             }
169         }
170         return new Pair<>(
171                 getBluetoothDrawable(context, com.android.internal.R.drawable.ic_settings_bluetooth)
172                         .mutate(),
173                 context.getString(R.string.bluetooth_talkback_bluetooth));
174     }
175 
176     /** Get bluetooth drawable by {@code resId} */
getBluetoothDrawable(Context context, @DrawableRes int resId)177     public static Drawable getBluetoothDrawable(Context context, @DrawableRes int resId) {
178         return context.getDrawable(resId);
179     }
180 
181     /** Get colorful bluetooth icon with description */
getBtRainbowDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)182     public static Pair<Drawable, String> getBtRainbowDrawableWithDescription(
183             Context context, CachedBluetoothDevice cachedDevice) {
184         final Resources resources = context.getResources();
185         final Pair<Drawable, String> pair =
186                 BluetoothUtils.getBtDrawableWithDescription(context, cachedDevice);
187 
188         if (pair.first instanceof BitmapDrawable) {
189             return new Pair<>(
190                     new AdaptiveOutlineDrawable(
191                             resources, ((BitmapDrawable) pair.first).getBitmap()),
192                     pair.second);
193         }
194 
195         int hashCode;
196         if ((cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)) {
197             hashCode = new Integer(cachedDevice.getGroupId()).hashCode();
198         } else {
199             hashCode = cachedDevice.getAddress().hashCode();
200         }
201 
202         return new Pair<>(buildBtRainbowDrawable(context, pair.first, hashCode), pair.second);
203     }
204 
205     /** Build Bluetooth device icon with rainbow */
buildBtRainbowDrawable( Context context, Drawable drawable, int hashCode)206     private static Drawable buildBtRainbowDrawable(
207             Context context, Drawable drawable, int hashCode) {
208         final Resources resources = context.getResources();
209 
210         // Deal with normal headset
211         final int[] iconFgColors = resources.getIntArray(R.array.bt_icon_fg_colors);
212         final int[] iconBgColors = resources.getIntArray(R.array.bt_icon_bg_colors);
213 
214         // get color index based on mac address
215         final int index = Math.abs(hashCode % iconBgColors.length);
216         drawable.setTint(iconFgColors[index]);
217         final Drawable adaptiveIcon = new AdaptiveIcon(context, drawable);
218         ((AdaptiveIcon) adaptiveIcon).setBackgroundColor(iconBgColors[index]);
219 
220         return adaptiveIcon;
221     }
222 
223     /** Get bluetooth icon with description */
getBtDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)224     public static Pair<Drawable, String> getBtDrawableWithDescription(
225             Context context, CachedBluetoothDevice cachedDevice) {
226         final Pair<Drawable, String> pair =
227                 BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice);
228         final BluetoothDevice bluetoothDevice = cachedDevice.getDevice();
229         final int iconSize =
230                 context.getResources().getDimensionPixelSize(R.dimen.bt_nearby_icon_size);
231         final Resources resources = context.getResources();
232 
233         // Deal with advanced device icon
234         if (isAdvancedDetailsHeader(bluetoothDevice)) {
235             final Uri iconUri = getUriMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON);
236             if (iconUri != null) {
237                 try {
238                     context.getContentResolver()
239                             .takePersistableUriPermission(
240                                     iconUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
241                 } catch (SecurityException e) {
242                     Log.e(TAG, "Failed to take persistable permission for: " + iconUri, e);
243                 }
244                 try {
245                     final Bitmap bitmap =
246                             MediaStore.Images.Media.getBitmap(
247                                     context.getContentResolver(), iconUri);
248                     if (bitmap != null) {
249                         final Bitmap resizedBitmap =
250                                 Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false);
251                         bitmap.recycle();
252                         return new Pair<>(
253                                 new BitmapDrawable(resources, resizedBitmap), pair.second);
254                     }
255                 } catch (IOException e) {
256                     Log.e(TAG, "Failed to get drawable for: " + iconUri, e);
257                 } catch (SecurityException e) {
258                     Log.e(TAG, "Failed to get permission for: " + iconUri, e);
259                 }
260             }
261         }
262 
263         return new Pair<>(pair.first, pair.second);
264     }
265 
266     /**
267      * Check if the Bluetooth device supports advanced metadata
268      *
269      * @param bluetoothDevice the BluetoothDevice to get metadata
270      * @return true if it supports advanced metadata, false otherwise.
271      */
isAdvancedDetailsHeader(@onNull BluetoothDevice bluetoothDevice)272     public static boolean isAdvancedDetailsHeader(@NonNull BluetoothDevice bluetoothDevice) {
273         if (!isAdvancedHeaderEnabled()) {
274             return false;
275         }
276         if (isUntetheredHeadset(bluetoothDevice)) {
277             return true;
278         }
279         if (Flags.enableDeterminingAdvancedDetailsHeaderWithMetadata()) {
280             // A FastPair device that use advanced details header must have METADATA_MAIN_ICON
281             if (getUriMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON) != null) {
282                 Log.d(TAG, "isAdvancedDetailsHeader is true with main icon uri");
283                 return true;
284             }
285             return false;
286         }
287         // The metadata is for Android S
288         String deviceType =
289                 getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE);
290         if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)
291                 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_WATCH)
292                 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_DEFAULT)
293                 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_STYLUS)) {
294             Log.d(TAG, "isAdvancedDetailsHeader: deviceType is " + deviceType);
295             return true;
296         }
297         return false;
298     }
299 
300     /**
301      * Check if the Bluetooth device is supports advanced metadata and an untethered headset
302      *
303      * @param bluetoothDevice the BluetoothDevice to get metadata
304      * @return true if it supports advanced metadata and an untethered headset, false otherwise.
305      */
isAdvancedUntetheredDevice(@onNull BluetoothDevice bluetoothDevice)306     public static boolean isAdvancedUntetheredDevice(@NonNull BluetoothDevice bluetoothDevice) {
307         if (!isAdvancedHeaderEnabled()) {
308             return false;
309         }
310         if (isUntetheredHeadset(bluetoothDevice)) {
311             return true;
312         }
313         if (!Flags.enableDeterminingAdvancedDetailsHeaderWithMetadata()) {
314             // The METADATA_IS_UNTETHERED_HEADSET of an untethered FastPair headset is always true,
315             // so there's no need to check the device type.
316             String deviceType =
317                     getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE);
318             if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) {
319                 Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device");
320                 return true;
321             }
322         }
323         return false;
324     }
325 
326     /**
327      * Check if a device class matches with a defined BluetoothClass device.
328      *
329      * @param device Must be one of the public constants in {@link BluetoothClass.Device}
330      * @return true if device class matches, false otherwise.
331      */
isDeviceClassMatched( @onNull BluetoothDevice bluetoothDevice, int device)332     public static boolean isDeviceClassMatched(
333             @NonNull BluetoothDevice bluetoothDevice, int device) {
334         final BluetoothClass bluetoothClass = bluetoothDevice.getBluetoothClass();
335         return bluetoothClass != null && bluetoothClass.getDeviceClass() == device;
336     }
337 
isAdvancedHeaderEnabled()338     private static boolean isAdvancedHeaderEnabled() {
339         if (!DeviceConfig.getBoolean(
340                 DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED, true)) {
341             Log.d(TAG, "isAdvancedDetailsHeader: advancedEnabled is false");
342             return false;
343         }
344         return true;
345     }
346 
isUntetheredHeadset(@onNull BluetoothDevice bluetoothDevice)347     private static boolean isUntetheredHeadset(@NonNull BluetoothDevice bluetoothDevice) {
348         // The metadata is for Android R
349         if (getBooleanMetaData(bluetoothDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
350             Log.d(TAG, "isAdvancedDetailsHeader: untetheredHeadset is true");
351             return true;
352         }
353         return false;
354     }
355 
356     /** Create an Icon pointing to a drawable. */
createIconWithDrawable(Drawable drawable)357     public static IconCompat createIconWithDrawable(Drawable drawable) {
358         Bitmap bitmap;
359         if (drawable instanceof BitmapDrawable) {
360             bitmap = ((BitmapDrawable) drawable).getBitmap();
361         } else {
362             final int width = drawable.getIntrinsicWidth();
363             final int height = drawable.getIntrinsicHeight();
364             bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1);
365         }
366         return IconCompat.createWithBitmap(bitmap);
367     }
368 
369     /** Build device icon with advanced outline */
buildAdvancedDrawable(Context context, Drawable drawable)370     public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) {
371         final int iconSize =
372                 context.getResources().getDimensionPixelSize(R.dimen.advanced_icon_size);
373         final Resources resources = context.getResources();
374 
375         Bitmap bitmap = null;
376         if (drawable instanceof BitmapDrawable) {
377             bitmap = ((BitmapDrawable) drawable).getBitmap();
378         } else {
379             final int width = drawable.getIntrinsicWidth();
380             final int height = drawable.getIntrinsicHeight();
381             bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1);
382         }
383 
384         if (bitmap != null) {
385             final Bitmap resizedBitmap =
386                     Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false);
387             bitmap.recycle();
388             return new AdaptiveOutlineDrawable(resources, resizedBitmap, ICON_TYPE_ADVANCED);
389         }
390 
391         return drawable;
392     }
393 
394     /** Creates a drawable with specified width and height. */
createBitmap(Drawable drawable, int width, int height)395     public static Bitmap createBitmap(Drawable drawable, int width, int height) {
396         final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
397         final Canvas canvas = new Canvas(bitmap);
398         drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
399         drawable.draw(canvas);
400         return bitmap;
401     }
402 
403     /**
404      * Get boolean Bluetooth metadata
405      *
406      * @param bluetoothDevice the BluetoothDevice to get metadata
407      * @param key key value within the list of BluetoothDevice.METADATA_*
408      * @return the boolean metdata
409      */
getBooleanMetaData(BluetoothDevice bluetoothDevice, int key)410     public static boolean getBooleanMetaData(BluetoothDevice bluetoothDevice, int key) {
411         if (bluetoothDevice == null) {
412             return false;
413         }
414         final byte[] data = bluetoothDevice.getMetadata(key);
415         if (data == null) {
416             return false;
417         }
418         return Boolean.parseBoolean(new String(data));
419     }
420 
421     /**
422      * Get String Bluetooth metadata
423      *
424      * @param bluetoothDevice the BluetoothDevice to get metadata
425      * @param key key value within the list of BluetoothDevice.METADATA_*
426      * @return the String metdata
427      */
getStringMetaData(BluetoothDevice bluetoothDevice, int key)428     public static String getStringMetaData(BluetoothDevice bluetoothDevice, int key) {
429         if (bluetoothDevice == null) {
430             return null;
431         }
432         final byte[] data = bluetoothDevice.getMetadata(key);
433         if (data == null) {
434             return null;
435         }
436         return new String(data);
437     }
438 
439     /**
440      * Get integer Bluetooth metadata
441      *
442      * @param bluetoothDevice the BluetoothDevice to get metadata
443      * @param key key value within the list of BluetoothDevice.METADATA_*
444      * @return the int metdata
445      */
getIntMetaData(BluetoothDevice bluetoothDevice, int key)446     public static int getIntMetaData(BluetoothDevice bluetoothDevice, int key) {
447         if (bluetoothDevice == null) {
448             return META_INT_ERROR;
449         }
450         final byte[] data = bluetoothDevice.getMetadata(key);
451         if (data == null) {
452             return META_INT_ERROR;
453         }
454         try {
455             return Integer.parseInt(new String(data));
456         } catch (NumberFormatException e) {
457             return META_INT_ERROR;
458         }
459     }
460 
461     /**
462      * Get URI Bluetooth metadata
463      *
464      * @param bluetoothDevice the BluetoothDevice to get metadata
465      * @param key key value within the list of BluetoothDevice.METADATA_*
466      * @return the URI metdata
467      */
getUriMetaData(BluetoothDevice bluetoothDevice, int key)468     public static Uri getUriMetaData(BluetoothDevice bluetoothDevice, int key) {
469         String data = getStringMetaData(bluetoothDevice, key);
470         if (data == null) {
471             return null;
472         }
473         return Uri.parse(data);
474     }
475 
476     /**
477      * Get URI Bluetooth metadata for extra control
478      *
479      * @param bluetoothDevice the BluetoothDevice to get metadata
480      * @return the URI metadata
481      */
getControlUriMetaData(BluetoothDevice bluetoothDevice)482     public static String getControlUriMetaData(BluetoothDevice bluetoothDevice) {
483         String data = getStringMetaData(bluetoothDevice, METADATA_FAST_PAIR_CUSTOMIZED_FIELDS);
484         return extraTagValue(KEY_HEARABLE_CONTROL_SLICE, data);
485     }
486 
487     /**
488      * Check if the Bluetooth device is an AvailableMediaBluetoothDevice, which means: 1) currently
489      * connected 2) is Hearing Aid or LE Audio OR 3) connected profile matches currentAudioProfile
490      *
491      * @param cachedDevice the CachedBluetoothDevice
492      * @param audioManager audio manager to get the current audio profile
493      * @return if the device is AvailableMediaBluetoothDevice
494      */
495     @WorkerThread
isAvailableMediaBluetoothDevice( CachedBluetoothDevice cachedDevice, AudioManager audioManager)496     public static boolean isAvailableMediaBluetoothDevice(
497             CachedBluetoothDevice cachedDevice, AudioManager audioManager) {
498         int audioMode = audioManager.getMode();
499         int currentAudioProfile;
500 
501         if (audioMode == AudioManager.MODE_RINGTONE
502                 || audioMode == AudioManager.MODE_IN_CALL
503                 || audioMode == AudioManager.MODE_IN_COMMUNICATION) {
504             // in phone call
505             currentAudioProfile = BluetoothProfile.HEADSET;
506         } else {
507             // without phone call
508             currentAudioProfile = BluetoothProfile.A2DP;
509         }
510 
511         boolean isFilterMatched = false;
512         if (isDeviceConnected(cachedDevice)) {
513             // If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP.
514             // It would show in Available Devices group.
515             if (cachedDevice.isConnectedAshaHearingAidDevice()
516                     || cachedDevice.isConnectedLeAudioDevice()) {
517                 Log.d(
518                         TAG,
519                         "isFilterMatched() device : "
520                                 + cachedDevice.getName()
521                                 + ", the profile is connected.");
522                 return true;
523             }
524             // According to the current audio profile type,
525             // this page will show the bluetooth device that have corresponding profile.
526             // For example:
527             // If current audio profile is a2dp, show the bluetooth device that have a2dp profile.
528             // If current audio profile is headset,
529             // show the bluetooth device that have headset profile.
530             switch (currentAudioProfile) {
531                 case BluetoothProfile.A2DP:
532                     isFilterMatched = cachedDevice.isConnectedA2dpDevice();
533                     break;
534                 case BluetoothProfile.HEADSET:
535                     isFilterMatched = cachedDevice.isConnectedHfpDevice();
536                     break;
537             }
538         }
539         return isFilterMatched;
540     }
541 
542     /** Returns if the le audio sharing is enabled. */
isAudioSharingEnabled()543     public static boolean isAudioSharingEnabled() {
544         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
545         try {
546             return Flags.enableLeAudioSharing()
547                     && adapter.isLeAudioBroadcastSourceSupported()
548                             == BluetoothStatusCodes.FEATURE_SUPPORTED
549                     && adapter.isLeAudioBroadcastAssistantSupported()
550                             == BluetoothStatusCodes.FEATURE_SUPPORTED;
551         } catch (IllegalStateException e) {
552             Log.d(TAG, "LE state is on, but there is no bluetooth service.", e);
553             return false;
554         }
555     }
556 
557     /** Returns if the broadcast is on-going. */
558     @WorkerThread
isBroadcasting(@ullable LocalBluetoothManager manager)559     public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
560         if (manager == null) return false;
561         LocalBluetoothLeBroadcast broadcast =
562                 manager.getProfileManager().getLeAudioBroadcastProfile();
563         return broadcast != null && broadcast.isEnabled(null);
564     }
565 
566     /**
567      * Check if {@link CachedBluetoothDevice} has connected to a broadcast source.
568      *
569      * @param cachedDevice The cached bluetooth device to check.
570      * @param localBtManager The BT manager to provide BT functions.
571      * @return Whether the device has connected to a broadcast source.
572      */
573     @WorkerThread
hasConnectedBroadcastSource( CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager)574     public static boolean hasConnectedBroadcastSource(
575             CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
576         if (localBtManager == null) {
577             Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null");
578             return false;
579         }
580         LocalBluetoothLeBroadcastAssistant assistant =
581                 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
582         if (assistant == null) {
583             Log.d(TAG, "Skip check hasConnectedBroadcastSource due to assistant profile is null");
584             return false;
585         }
586         List<BluetoothLeBroadcastReceiveState> sourceList =
587                 assistant.getAllSources(cachedDevice.getDevice());
588         if (!sourceList.isEmpty() && sourceList.stream().anyMatch(BluetoothUtils::isConnected)) {
589             Log.d(
590                     TAG,
591                     "Lead device has connected broadcast source, device = "
592                             + cachedDevice.getDevice().getAnonymizedAddress());
593             return true;
594         }
595         // Return true if member device is in broadcast.
596         for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
597             List<BluetoothLeBroadcastReceiveState> list =
598                     assistant.getAllSources(device.getDevice());
599             if (!list.isEmpty() && list.stream().anyMatch(BluetoothUtils::isConnected)) {
600                 Log.d(
601                         TAG,
602                         "Member device has connected broadcast source, device = "
603                                 + device.getDevice().getAnonymizedAddress());
604                 return true;
605             }
606         }
607         return false;
608     }
609 
610     /** Checks the connectivity status based on the provided broadcast receive state. */
611     @WorkerThread
isConnected(BluetoothLeBroadcastReceiveState state)612     public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
613         return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0);
614     }
615 
616     /**
617      * Checks if the Bluetooth device is an available hearing device, which means: 1) currently
618      * connected 2) is Hearing Aid 3) connected profile match hearing aid related profiles (e.g.
619      * ASHA, HAP)
620      *
621      * @param cachedDevice the CachedBluetoothDevice
622      * @return if the device is Available hearing device
623      */
624     @WorkerThread
isAvailableHearingDevice(CachedBluetoothDevice cachedDevice)625     public static boolean isAvailableHearingDevice(CachedBluetoothDevice cachedDevice) {
626         if (isDeviceConnected(cachedDevice) && cachedDevice.isConnectedHearingAidDevice()) {
627             Log.d(
628                     TAG,
629                     "isFilterMatched() device : "
630                             + cachedDevice.getName()
631                             + ", the profile is connected.");
632             return true;
633         }
634         return false;
635     }
636 
637     /**
638      * Check if the Bluetooth device is a ConnectedBluetoothDevice, which means: 1) currently
639      * connected 2) is not Hearing Aid or LE Audio AND 3) connected profile does not match
640      * currentAudioProfile
641      *
642      * @param cachedDevice the CachedBluetoothDevice
643      * @param audioManager audio manager to get the current audio profile
644      * @return if the device is AvailableMediaBluetoothDevice
645      */
646     @WorkerThread
isConnectedBluetoothDevice( CachedBluetoothDevice cachedDevice, AudioManager audioManager)647     public static boolean isConnectedBluetoothDevice(
648             CachedBluetoothDevice cachedDevice, AudioManager audioManager) {
649         int audioMode = audioManager.getMode();
650         int currentAudioProfile;
651 
652         if (audioMode == AudioManager.MODE_RINGTONE
653                 || audioMode == AudioManager.MODE_IN_CALL
654                 || audioMode == AudioManager.MODE_IN_COMMUNICATION) {
655             // in phone call
656             currentAudioProfile = BluetoothProfile.HEADSET;
657         } else {
658             // without phone call
659             currentAudioProfile = BluetoothProfile.A2DP;
660         }
661 
662         boolean isFilterMatched = false;
663         if (isDeviceConnected(cachedDevice)) {
664             // If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP.
665             // It would not show in Connected Devices group.
666             if (cachedDevice.isConnectedAshaHearingAidDevice()
667                     || cachedDevice.isConnectedLeAudioDevice()) {
668                 return false;
669             }
670             // According to the current audio profile type,
671             // this page will show the bluetooth device that doesn't have corresponding profile.
672             // For example:
673             // If current audio profile is a2dp,
674             // show the bluetooth device that doesn't have a2dp profile.
675             // If current audio profile is headset,
676             // show the bluetooth device that doesn't have headset profile.
677             switch (currentAudioProfile) {
678                 case BluetoothProfile.A2DP:
679                     isFilterMatched = !cachedDevice.isConnectedA2dpDevice();
680                     break;
681                 case BluetoothProfile.HEADSET:
682                     isFilterMatched = !cachedDevice.isConnectedHfpDevice();
683                     break;
684             }
685         }
686         return isFilterMatched;
687     }
688 
689     /**
690      * Check if the Bluetooth device is an active media device
691      *
692      * @param cachedDevice the CachedBluetoothDevice
693      * @return if the Bluetooth device is an active media device
694      */
isActiveMediaDevice(CachedBluetoothDevice cachedDevice)695     public static boolean isActiveMediaDevice(CachedBluetoothDevice cachedDevice) {
696         return cachedDevice.isActiveDevice(BluetoothProfile.A2DP)
697                 || cachedDevice.isActiveDevice(BluetoothProfile.HEADSET)
698                 || cachedDevice.isActiveDevice(BluetoothProfile.HEARING_AID)
699                 || cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
700     }
701 
702     /**
703      * Check if the Bluetooth device is an active LE Audio device
704      *
705      * @param cachedDevice the CachedBluetoothDevice
706      * @return if the Bluetooth device is an active LE Audio device
707      */
isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice)708     public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) {
709         return cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
710     }
711 
isDeviceConnected(CachedBluetoothDevice cachedDevice)712     private static boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) {
713         if (cachedDevice == null) {
714             return false;
715         }
716         final BluetoothDevice device = cachedDevice.getDevice();
717         return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected();
718     }
719 
720     @SuppressLint("NewApi") // Hidden API made public
doesClassMatch(BluetoothClass btClass, int classId)721     private static boolean doesClassMatch(BluetoothClass btClass, int classId) {
722         return btClass.doesClassMatch(classId);
723     }
724 
extraTagValue(String tag, String metaData)725     private static String extraTagValue(String tag, String metaData) {
726         if (TextUtils.isEmpty(metaData)) {
727             return null;
728         }
729         Pattern pattern = Pattern.compile(generateExpressionWithTag(tag, "(.*?)"));
730         Matcher matcher = pattern.matcher(metaData);
731         if (matcher.find()) {
732             return matcher.group(1);
733         }
734         return null;
735     }
736 
getTagStart(String tag)737     private static String getTagStart(String tag) {
738         return String.format(Locale.ENGLISH, "<%s>", tag);
739     }
740 
getTagEnd(String tag)741     private static String getTagEnd(String tag) {
742         return String.format(Locale.ENGLISH, "</%s>", tag);
743     }
744 
generateExpressionWithTag(String tag, String value)745     private static String generateExpressionWithTag(String tag, String value) {
746         return getTagStart(tag) + value + getTagEnd(tag);
747     }
748 
749     /**
750      * Returns the BluetoothDevice's exclusive manager ({@link
751      * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists, otherwise null.
752      */
753     @Nullable
getExclusiveManager(BluetoothDevice bluetoothDevice)754     private static String getExclusiveManager(BluetoothDevice bluetoothDevice) {
755         byte[] exclusiveManagerBytes =
756                 bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER);
757         if (exclusiveManagerBytes == null) {
758             Log.d(
759                     TAG,
760                     "Bluetooth device "
761                             + bluetoothDevice.getName()
762                             + " doesn't have exclusive manager");
763             return null;
764         }
765         return new String(exclusiveManagerBytes);
766     }
767 
768     /** Checks if given package is installed and enabled */
isPackageInstalledAndEnabled(Context context, String packageName)769     private static boolean isPackageInstalledAndEnabled(Context context, String packageName) {
770         PackageManager packageManager = context.getPackageManager();
771         try {
772             ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0);
773             return appInfo.enabled;
774         } catch (PackageManager.NameNotFoundException e) {
775             Log.d(TAG, "Package " + packageName + " is not installed/enabled");
776         }
777         return false;
778     }
779 
780     /**
781      * A BluetoothDevice is exclusively managed if 1) it has field {@link
782      * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata. 2) the exclusive manager app is
783      * installed and enabled.
784      */
isExclusivelyManagedBluetoothDevice( @onNull Context context, @NonNull BluetoothDevice bluetoothDevice)785     public static boolean isExclusivelyManagedBluetoothDevice(
786             @NonNull Context context, @NonNull BluetoothDevice bluetoothDevice) {
787         String exclusiveManagerName = getExclusiveManager(bluetoothDevice);
788         if (exclusiveManagerName == null) {
789             return false;
790         }
791 
792         ComponentName exclusiveManagerComponent =
793                 ComponentName.unflattenFromString(exclusiveManagerName);
794         String exclusiveManagerPackage = exclusiveManagerComponent != null
795                 ? exclusiveManagerComponent.getPackageName() : exclusiveManagerName;
796 
797         if (!isPackageInstalledAndEnabled(context, exclusiveManagerPackage)) {
798             return false;
799         } else {
800             Log.d(TAG, "Found exclusively managed app " + exclusiveManagerPackage);
801             return true;
802         }
803     }
804 
805     /**
806      * Get CSIP group id for {@link CachedBluetoothDevice}.
807      *
808      * <p>If CachedBluetoothDevice#getGroupId is invalid, fetch group id from
809      * LeAudioProfile#getGroupId.
810      */
getGroupId(@onNull CachedBluetoothDevice cachedDevice)811     public static int getGroupId(@NonNull CachedBluetoothDevice cachedDevice) {
812         int groupId = cachedDevice.getGroupId();
813         String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress();
814         if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
815             Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress);
816             return groupId;
817         }
818         for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
819             if (profile instanceof LeAudioProfile) {
820                 Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress);
821                 return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice());
822             }
823         }
824         Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress);
825         return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
826     }
827 }
828