• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.settingslib.bluetooth;
2 
3 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.UNKNOWN_VALUE_PLACEHOLDER;
4 import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
5 import static com.android.settingslib.widget.AdaptiveOutlineDrawable.ICON_TYPE_ADVANCED;
6 
7 import android.annotation.SuppressLint;
8 import android.bluetooth.BluetoothAdapter;
9 import android.bluetooth.BluetoothClass;
10 import android.bluetooth.BluetoothCsipSetCoordinator;
11 import android.bluetooth.BluetoothDevice;
12 import android.bluetooth.BluetoothLeBroadcastReceiveState;
13 import android.bluetooth.BluetoothProfile;
14 import android.bluetooth.BluetoothStatusCodes;
15 import android.content.ComponentName;
16 import android.content.ContentResolver;
17 import android.content.Context;
18 import android.content.Intent;
19 import android.content.pm.ApplicationInfo;
20 import android.content.pm.PackageManager;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.Canvas;
24 import android.graphics.drawable.BitmapDrawable;
25 import android.graphics.drawable.Drawable;
26 import android.hardware.input.InputManager;
27 import android.media.AudioDeviceAttributes;
28 import android.media.AudioDeviceInfo;
29 import android.media.AudioManager;
30 import android.net.Uri;
31 import android.provider.DeviceConfig;
32 import android.provider.MediaStore;
33 import android.provider.Settings;
34 import android.sysprop.BluetoothProperties;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.util.Pair;
38 import android.view.InputDevice;
39 
40 import androidx.annotation.DrawableRes;
41 import androidx.annotation.NonNull;
42 import androidx.annotation.Nullable;
43 import androidx.annotation.WorkerThread;
44 import androidx.core.graphics.drawable.IconCompat;
45 
46 import com.android.settingslib.R;
47 import com.android.settingslib.flags.Flags;
48 import com.android.settingslib.widget.AdaptiveIcon;
49 import com.android.settingslib.widget.AdaptiveOutlineDrawable;
50 
51 import com.google.common.collect.ImmutableSet;
52 
53 import java.io.IOException;
54 import java.lang.reflect.InvocationTargetException;
55 import java.lang.reflect.Method;
56 import java.util.List;
57 import java.util.Locale;
58 import java.util.Objects;
59 import java.util.Optional;
60 import java.util.Set;
61 import java.util.regex.Matcher;
62 import java.util.regex.Pattern;
63 import java.util.stream.Collectors;
64 
65 public class BluetoothUtils {
66     private static final String TAG = "BluetoothUtils";
67 
68     public static final boolean V = false; // verbose logging
69     public static final boolean D = true; // regular logging
70 
71     public static final int META_INT_ERROR = -1;
72     public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled";
73     public static final String DEVELOPER_OPTION_PREVIEW_KEY =
74             "bluetooth_le_audio_sharing_ui_preview_enabled";
75     private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
76     private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH";
77     private static final Set<Integer> SA_PROFILES =
78             ImmutableSet.of(
79                     BluetoothProfile.A2DP, BluetoothProfile.LE_AUDIO, BluetoothProfile.HEARING_AID);
80     private static final List<Integer> BLUETOOTH_DEVICE_CLASS_HEADSET =
81             List.of(
82                     BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES,
83                     BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET);
84 
85     private static final String TEMP_BOND_TYPE = "TEMP_BOND_TYPE";
86     private static final String TEMP_BOND_DEVICE_METADATA_VALUE = "le_audio_sharing";
87 
88     private static ErrorListener sErrorListener;
89 
getConnectionStateSummary(int connectionState)90     public static int getConnectionStateSummary(int connectionState) {
91         switch (connectionState) {
92             case BluetoothProfile.STATE_CONNECTED:
93                 return R.string.bluetooth_connected;
94             case BluetoothProfile.STATE_CONNECTING:
95                 return R.string.bluetooth_connecting;
96             case BluetoothProfile.STATE_DISCONNECTED:
97                 return R.string.bluetooth_disconnected;
98             case BluetoothProfile.STATE_DISCONNECTING:
99                 return R.string.bluetooth_disconnecting;
100             default:
101                 return 0;
102         }
103     }
104 
showError(Context context, String name, int messageResId)105     static void showError(Context context, String name, int messageResId) {
106         if (sErrorListener != null) {
107             sErrorListener.onShowError(context, name, messageResId);
108         }
109     }
110 
setErrorListener(ErrorListener listener)111     public static void setErrorListener(ErrorListener listener) {
112         sErrorListener = listener;
113     }
114 
115     public interface ErrorListener {
onShowError(Context context, String name, int messageResId)116         void onShowError(Context context, String name, int messageResId);
117     }
118 
119     /**
120      * @param context to access resources from
121      * @param cachedDevice to get class from
122      * @return pair containing the drawable and the description of the type of the device. The type
123      *     could either derived from metadata or CoD.
124      */
getDerivedBtClassDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)125     public static Pair<Drawable, String> getDerivedBtClassDrawableWithDescription(
126             Context context, CachedBluetoothDevice cachedDevice) {
127         return BluetoothUtils.isAdvancedUntetheredDevice(cachedDevice.getDevice())
128                 ? new Pair<>(
129                         getBluetoothDrawable(
130                                 context, com.android.internal.R.drawable.ic_bt_headphones_a2dp),
131                         context.getString(R.string.bluetooth_talkback_headphone))
132                 : BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice);
133     }
134 
135     /**
136      * @param context to access resources from
137      * @param cachedDevice to get class from
138      * @return pair containing the drawable and the description of the Bluetooth class of the
139      *     device.
140      */
getBtClassDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)141     public static Pair<Drawable, String> getBtClassDrawableWithDescription(
142             Context context, CachedBluetoothDevice cachedDevice) {
143         BluetoothClass btClass = cachedDevice.getBtClass();
144         if (btClass != null) {
145             switch (btClass.getMajorDeviceClass()) {
146                 case BluetoothClass.Device.Major.COMPUTER:
147                     return new Pair<>(
148                             getBluetoothDrawable(
149                                     context, com.android.internal.R.drawable.ic_bt_laptop),
150                             context.getString(R.string.bluetooth_talkback_computer));
151 
152                 case BluetoothClass.Device.Major.PHONE:
153                     return new Pair<>(
154                             getBluetoothDrawable(context, com.android.internal.R.drawable.ic_phone),
155                             context.getString(R.string.bluetooth_talkback_phone));
156 
157                 case BluetoothClass.Device.Major.PERIPHERAL:
158                     return new Pair<>(
159                             getBluetoothDrawable(context, HidProfile.getHidClassDrawable(btClass)),
160                             context.getString(R.string.bluetooth_talkback_input_peripheral));
161 
162                 case BluetoothClass.Device.Major.IMAGING:
163                     return new Pair<>(
164                             getBluetoothDrawable(
165                                     context, com.android.internal.R.drawable.ic_settings_print),
166                             context.getString(R.string.bluetooth_talkback_imaging));
167 
168                 default:
169                     // unrecognized device class; continue
170             }
171         }
172 
173         if (cachedDevice.isHearingAidDevice()) {
174             return new Pair<>(
175                     getBluetoothDrawable(
176                             context, com.android.internal.R.drawable.ic_bt_hearing_aid),
177                     context.getString(R.string.bluetooth_talkback_hearing_aids));
178         }
179 
180         List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles();
181         int resId = 0;
182         for (LocalBluetoothProfile profile : profiles) {
183             int profileResId = profile.getDrawableResource(btClass);
184             if (profileResId != 0) {
185                 // The device should show hearing aid icon if it contains any hearing aid related
186                 // profiles
187                 if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) {
188                     return new Pair<>(
189                             getBluetoothDrawable(context, profileResId),
190                             context.getString(R.string.bluetooth_talkback_hearing_aids));
191                 }
192                 if (resId == 0) {
193                     resId = profileResId;
194                 }
195             }
196         }
197         if (resId != 0) {
198             return new Pair<>(getBluetoothDrawable(context, resId), null);
199         }
200 
201         if (btClass != null) {
202             if (doesClassMatch(btClass, BluetoothClass.PROFILE_HEADSET)) {
203                 return new Pair<>(
204                         getBluetoothDrawable(
205                                 context, com.android.internal.R.drawable.ic_bt_headset_hfp),
206                         context.getString(R.string.bluetooth_talkback_headset));
207             }
208             if (doesClassMatch(btClass, BluetoothClass.PROFILE_A2DP)) {
209                 return new Pair<>(
210                         getBluetoothDrawable(
211                                 context, com.android.internal.R.drawable.ic_bt_headphones_a2dp),
212                         context.getString(R.string.bluetooth_talkback_headphone));
213             }
214         }
215         return new Pair<>(
216                 getBluetoothDrawable(context, com.android.internal.R.drawable.ic_settings_bluetooth)
217                         .mutate(),
218                 context.getString(R.string.bluetooth_talkback_bluetooth));
219     }
220 
221     /** Get bluetooth drawable by {@code resId} */
getBluetoothDrawable(Context context, @DrawableRes int resId)222     public static Drawable getBluetoothDrawable(Context context, @DrawableRes int resId) {
223         return context.getDrawable(resId);
224     }
225 
226     /** Get colorful bluetooth icon with description */
getBtRainbowDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)227     public static Pair<Drawable, String> getBtRainbowDrawableWithDescription(
228             Context context, CachedBluetoothDevice cachedDevice) {
229         final Resources resources = context.getResources();
230         final Pair<Drawable, String> pair =
231                 BluetoothUtils.getBtDrawableWithDescription(context, cachedDevice);
232 
233         if (pair.first instanceof BitmapDrawable) {
234             return new Pair<>(
235                     new AdaptiveOutlineDrawable(
236                             resources, ((BitmapDrawable) pair.first).getBitmap()),
237                     pair.second);
238         }
239 
240         int hashCode;
241         if ((cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)) {
242             hashCode = new Integer(cachedDevice.getGroupId()).hashCode();
243         } else {
244             hashCode = cachedDevice.getAddress().hashCode();
245         }
246 
247         return new Pair<>(buildBtRainbowDrawable(context, pair.first, hashCode), pair.second);
248     }
249 
250     /** Build Bluetooth device icon with rainbow */
buildBtRainbowDrawable( Context context, Drawable drawable, int hashCode)251     private static Drawable buildBtRainbowDrawable(
252             Context context, Drawable drawable, int hashCode) {
253         final Resources resources = context.getResources();
254 
255         // Deal with normal headset
256         final int[] iconFgColors = resources.getIntArray(R.array.bt_icon_fg_colors);
257         final int[] iconBgColors = resources.getIntArray(R.array.bt_icon_bg_colors);
258 
259         // get color index based on mac address
260         final int index = Math.abs(hashCode % iconBgColors.length);
261         drawable.setTint(iconFgColors[index]);
262         final Drawable adaptiveIcon = new AdaptiveIcon(context, drawable);
263         ((AdaptiveIcon) adaptiveIcon).setBackgroundColor(iconBgColors[index]);
264 
265         return adaptiveIcon;
266     }
267 
268     /** Get bluetooth icon with description */
getBtDrawableWithDescription( Context context, CachedBluetoothDevice cachedDevice)269     public static Pair<Drawable, String> getBtDrawableWithDescription(
270             Context context, CachedBluetoothDevice cachedDevice) {
271         final Pair<Drawable, String> pair =
272                 BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice);
273         final BluetoothDevice bluetoothDevice = cachedDevice.getDevice();
274         final int iconSize =
275                 context.getResources().getDimensionPixelSize(R.dimen.bt_nearby_icon_size);
276         final Resources resources = context.getResources();
277 
278         // Deal with advanced device icon
279         if (isAdvancedDetailsHeader(bluetoothDevice)) {
280             final Uri iconUri = getUriMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON);
281             if (iconUri != null) {
282                 try {
283                     context.getContentResolver()
284                             .takePersistableUriPermission(
285                                     iconUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
286                 } catch (SecurityException e) {
287                     Log.e(TAG, "Failed to take persistable permission for: " + iconUri, e);
288                 }
289                 try {
290                     final Bitmap bitmap =
291                             MediaStore.Images.Media.getBitmap(
292                                     context.getContentResolver(), iconUri);
293                     if (bitmap != null) {
294                         final Bitmap resizedBitmap =
295                                 Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false);
296                         bitmap.recycle();
297                         return new Pair<>(
298                                 new BitmapDrawable(resources, resizedBitmap), pair.second);
299                     }
300                 } catch (IOException e) {
301                     Log.e(TAG, "Failed to get drawable for: " + iconUri, e);
302                 } catch (SecurityException e) {
303                     Log.e(TAG, "Failed to get permission for: " + iconUri, e);
304                 }
305             }
306         }
307 
308         return new Pair<>(pair.first, pair.second);
309     }
310 
311     /**
312      * Check if the Bluetooth device supports advanced metadata
313      *
314      * @param bluetoothDevice the BluetoothDevice to get metadata
315      * @return true if it supports advanced metadata, false otherwise.
316      */
isAdvancedDetailsHeader(@onNull BluetoothDevice bluetoothDevice)317     public static boolean isAdvancedDetailsHeader(@NonNull BluetoothDevice bluetoothDevice) {
318         if (!isAdvancedHeaderEnabled()) {
319             return false;
320         }
321         if (isUntetheredHeadset(bluetoothDevice)) {
322             return true;
323         }
324         if (Flags.enableDeterminingAdvancedDetailsHeaderWithMetadata()) {
325             // A FastPair device that use advanced details header must have METADATA_MAIN_ICON
326             if (getUriMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON) != null) {
327                 Log.d(TAG, "isAdvancedDetailsHeader is true with main icon uri");
328                 return true;
329             }
330             return false;
331         }
332         // The metadata is for Android S
333         String deviceType =
334                 getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE);
335         if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)
336                 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_WATCH)
337                 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_DEFAULT)
338                 || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_STYLUS)) {
339             Log.d(TAG, "isAdvancedDetailsHeader: deviceType is " + deviceType);
340             return true;
341         }
342         return false;
343     }
344 
345     /**
346      * Check if the Bluetooth device is supports advanced metadata and an untethered headset
347      *
348      * @param bluetoothDevice the BluetoothDevice to get metadata
349      * @return true if it supports advanced metadata and an untethered headset, false otherwise.
350      */
isAdvancedUntetheredDevice(@onNull BluetoothDevice bluetoothDevice)351     public static boolean isAdvancedUntetheredDevice(@NonNull BluetoothDevice bluetoothDevice) {
352         if (!isAdvancedHeaderEnabled()) {
353             return false;
354         }
355         if (isUntetheredHeadset(bluetoothDevice)) {
356             return true;
357         }
358         if (!Flags.enableDeterminingAdvancedDetailsHeaderWithMetadata()) {
359             // The METADATA_IS_UNTETHERED_HEADSET of an untethered FastPair headset is always true,
360             // so there's no need to check the device type.
361             String deviceType =
362                     getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE);
363             if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) {
364                 Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device");
365                 return true;
366             }
367         }
368         return false;
369     }
370 
371     /**
372      * Check if a device class matches with a defined BluetoothClass device.
373      *
374      * @param device Must be one of the public constants in {@link BluetoothClass.Device}
375      * @return true if device class matches, false otherwise.
376      */
isDeviceClassMatched( @onNull BluetoothDevice bluetoothDevice, int device)377     public static boolean isDeviceClassMatched(
378             @NonNull BluetoothDevice bluetoothDevice, int device) {
379         final BluetoothClass bluetoothClass = bluetoothDevice.getBluetoothClass();
380         return bluetoothClass != null && bluetoothClass.getDeviceClass() == device;
381     }
382 
isAdvancedHeaderEnabled()383     private static boolean isAdvancedHeaderEnabled() {
384         if (!DeviceConfig.getBoolean(
385                 DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED, true)) {
386             Log.d(TAG, "isAdvancedDetailsHeader: advancedEnabled is false");
387             return false;
388         }
389         return true;
390     }
391 
isUntetheredHeadset(@onNull BluetoothDevice bluetoothDevice)392     private static boolean isUntetheredHeadset(@NonNull BluetoothDevice bluetoothDevice) {
393         // The metadata is for Android R
394         if (getBooleanMetaData(bluetoothDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
395             Log.d(TAG, "isAdvancedDetailsHeader: untetheredHeadset is true");
396             return true;
397         }
398         return false;
399     }
400 
401     /** Checks whether the bluetooth device is a headset. */
isHeadset(@onNull BluetoothDevice bluetoothDevice)402     public static boolean isHeadset(@NonNull BluetoothDevice bluetoothDevice) {
403         String deviceType =
404                 BluetoothUtils.getStringMetaData(
405                         bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE);
406         if (!TextUtils.isEmpty(deviceType)) {
407             return BluetoothDevice.DEVICE_TYPE_HEADSET.equals(deviceType)
408                     || BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.equals(deviceType);
409         }
410         BluetoothClass btClass = bluetoothDevice.getBluetoothClass();
411         return btClass != null && BLUETOOTH_DEVICE_CLASS_HEADSET.contains(btClass.getDeviceClass());
412     }
413 
414     /** Create an Icon pointing to a drawable. */
createIconWithDrawable(Drawable drawable)415     public static IconCompat createIconWithDrawable(Drawable drawable) {
416         Bitmap bitmap;
417         if (drawable instanceof BitmapDrawable) {
418             bitmap = ((BitmapDrawable) drawable).getBitmap();
419         } else {
420             final int width = drawable.getIntrinsicWidth();
421             final int height = drawable.getIntrinsicHeight();
422             bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1);
423         }
424         return IconCompat.createWithBitmap(bitmap);
425     }
426 
427     /** Build device icon with advanced outline */
buildAdvancedDrawable(Context context, Drawable drawable)428     public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) {
429         final int iconSize =
430                 context.getResources().getDimensionPixelSize(R.dimen.advanced_icon_size);
431         final Resources resources = context.getResources();
432 
433         Bitmap bitmap = null;
434         if (drawable instanceof BitmapDrawable) {
435             bitmap = ((BitmapDrawable) drawable).getBitmap();
436         } else {
437             final int width = drawable.getIntrinsicWidth();
438             final int height = drawable.getIntrinsicHeight();
439             bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1);
440         }
441 
442         if (bitmap != null) {
443             final Bitmap resizedBitmap =
444                     Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false);
445             bitmap.recycle();
446             return new AdaptiveOutlineDrawable(resources, resizedBitmap, ICON_TYPE_ADVANCED);
447         }
448 
449         return drawable;
450     }
451 
452     /** Creates a drawable with specified width and height. */
createBitmap(Drawable drawable, int width, int height)453     public static Bitmap createBitmap(Drawable drawable, int width, int height) {
454         final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
455         final Canvas canvas = new Canvas(bitmap);
456         drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
457         drawable.draw(canvas);
458         return bitmap;
459     }
460 
461     /**
462      * Get boolean 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 boolean metdata
467      */
getBooleanMetaData(BluetoothDevice bluetoothDevice, int key)468     public static boolean getBooleanMetaData(BluetoothDevice bluetoothDevice, int key) {
469         if (bluetoothDevice == null) {
470             return false;
471         }
472         final byte[] data = bluetoothDevice.getMetadata(key);
473         if (data == null) {
474             return false;
475         }
476         return Boolean.parseBoolean(new String(data));
477     }
478 
479     /**
480      * Get String Bluetooth metadata
481      *
482      * @param bluetoothDevice the BluetoothDevice to get metadata
483      * @param key key value within the list of BluetoothDevice.METADATA_*
484      * @return the String metdata
485      */
getStringMetaData(BluetoothDevice bluetoothDevice, int key)486     public static String getStringMetaData(BluetoothDevice bluetoothDevice, int key) {
487         if (bluetoothDevice == null) {
488             return null;
489         }
490         final byte[] data = bluetoothDevice.getMetadata(key);
491         if (data == null) {
492             return null;
493         }
494         return new String(data);
495     }
496 
497     /**
498      * Get integer Bluetooth metadata
499      *
500      * @param bluetoothDevice the BluetoothDevice to get metadata
501      * @param key key value within the list of BluetoothDevice.METADATA_*
502      * @return the int metdata
503      */
getIntMetaData(BluetoothDevice bluetoothDevice, int key)504     public static int getIntMetaData(BluetoothDevice bluetoothDevice, int key) {
505         if (bluetoothDevice == null) {
506             return META_INT_ERROR;
507         }
508         final byte[] data = bluetoothDevice.getMetadata(key);
509         if (data == null) {
510             return META_INT_ERROR;
511         }
512         try {
513             return Integer.parseInt(new String(data));
514         } catch (NumberFormatException e) {
515             return META_INT_ERROR;
516         }
517     }
518 
519     /**
520      * Get URI Bluetooth metadata
521      *
522      * @param bluetoothDevice the BluetoothDevice to get metadata
523      * @param key key value within the list of BluetoothDevice.METADATA_*
524      * @return the URI metdata
525      */
getUriMetaData(BluetoothDevice bluetoothDevice, int key)526     public static Uri getUriMetaData(BluetoothDevice bluetoothDevice, int key) {
527         String data = getStringMetaData(bluetoothDevice, key);
528         if (data == null) {
529             return null;
530         }
531         return Uri.parse(data);
532     }
533 
534     /**
535      * Gets string metadata from Fast Pair customized fields.
536      *
537      * @param bluetoothDevice the BluetoothDevice to get metadata
538      * @return the string metadata
539      */
540     @Nullable
getFastPairCustomizedField( @ullable BluetoothDevice bluetoothDevice, @NonNull String key)541     public static String getFastPairCustomizedField(
542             @Nullable BluetoothDevice bluetoothDevice, @NonNull String key) {
543         String data = getStringMetaData(bluetoothDevice, METADATA_FAST_PAIR_CUSTOMIZED_FIELDS);
544         return extraTagValue(key, data);
545     }
546 
547     /**
548      * Get URI Bluetooth metadata for extra control
549      *
550      * @param bluetoothDevice the BluetoothDevice to get metadata
551      * @return the URI metadata
552      */
getControlUriMetaData(BluetoothDevice bluetoothDevice)553     public static String getControlUriMetaData(BluetoothDevice bluetoothDevice) {
554         return getFastPairCustomizedField(bluetoothDevice, KEY_HEARABLE_CONTROL_SLICE);
555     }
556 
557     /**
558      * Check if the Bluetooth device is an AvailableMediaBluetoothDevice, which means: 1) currently
559      * connected 2) is Hearing Aid or LE Audio OR 3) connected profile matches currentAudioProfile
560      *
561      * @param cachedDevice the CachedBluetoothDevice
562      * @param isOngoingCall get the current audio profile based on if in phone call
563      * @return if the device is AvailableMediaBluetoothDevice
564      */
565     @WorkerThread
isAvailableMediaBluetoothDevice( CachedBluetoothDevice cachedDevice, boolean isOngoingCall)566     public static boolean isAvailableMediaBluetoothDevice(
567             CachedBluetoothDevice cachedDevice, boolean isOngoingCall) {
568         int currentAudioProfile;
569 
570         if (isOngoingCall) {
571             currentAudioProfile = BluetoothProfile.HEADSET;
572         } else {
573             currentAudioProfile = BluetoothProfile.A2DP;
574         }
575 
576         boolean isFilterMatched = false;
577         if (isDeviceConnected(cachedDevice)) {
578             // If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP.
579             // It would show in Available Devices group.
580             if (cachedDevice.isConnectedAshaHearingAidDevice()
581                     || cachedDevice.isConnectedLeAudioDevice()) {
582                 Log.d(
583                         TAG,
584                         "isFilterMatched() device : "
585                                 + cachedDevice.getName()
586                                 + ", the profile is connected.");
587                 return true;
588             }
589             // According to the current audio profile type,
590             // this page will show the bluetooth device that have corresponding profile.
591             // For example:
592             // If current audio profile is a2dp, show the bluetooth device that have a2dp profile.
593             // If current audio profile is headset,
594             // show the bluetooth device that have headset profile.
595             switch (currentAudioProfile) {
596                 case BluetoothProfile.A2DP:
597                     isFilterMatched = cachedDevice.isConnectedA2dpDevice();
598                     break;
599                 case BluetoothProfile.HEADSET:
600                     isFilterMatched = cachedDevice.isConnectedHfpDevice();
601                     break;
602             }
603         }
604         return isFilterMatched;
605     }
606 
607     /**
608      * Checks if a given `CachedBluetoothDevice` is available for audio sharing and being switch as
609      * active media device.
610      *
611      * <p>This method determines if the device meets the following criteria to be available:
612      *
613      * <ol>
614      *   <li>Audio sharing session is off.
615      *   <li>The device is one of the two connected devices on the LE Broadcast Assistant profile.
616      *   <li>The device is not currently active on the LE Audio profile.
617      *   <li>There is exactly one other device that is active on the LE Audio profile.
618      * </ol>
619      *
620      * @param cachedDevice The `CachedBluetoothDevice` to check.
621      * @param localBluetoothManager The `LocalBluetoothManager` instance, or null if unavailable.
622      * @return `true` if the device is available for audio sharing and settings as active, `false`
623      *     otherwise.
624      */
625     @WorkerThread
isAvailableAudioSharingMediaBluetoothDevice( CachedBluetoothDevice cachedDevice, @Nullable LocalBluetoothManager localBluetoothManager)626     public static boolean isAvailableAudioSharingMediaBluetoothDevice(
627             CachedBluetoothDevice cachedDevice,
628             @Nullable LocalBluetoothManager localBluetoothManager) {
629         LocalBluetoothLeBroadcastAssistant assistantProfile =
630                 Optional.ofNullable(localBluetoothManager)
631                         .map(LocalBluetoothManager::getProfileManager)
632                         .map(LocalBluetoothProfileManager::getLeAudioBroadcastAssistantProfile)
633                         .orElse(null);
634         LeAudioProfile leAudioProfile =
635                 Optional.ofNullable(localBluetoothManager)
636                         .map(LocalBluetoothManager::getProfileManager)
637                         .map(LocalBluetoothProfileManager::getLeAudioProfile)
638                         .orElse(null);
639         CachedBluetoothDeviceManager deviceManager =
640                 Optional.ofNullable(localBluetoothManager)
641                         .map(LocalBluetoothManager::getCachedDeviceManager)
642                         .orElse(null);
643         // If any of the profiles are null, or broadcast is already on, return false
644         if (assistantProfile == null
645                 || leAudioProfile == null
646                 || deviceManager == null
647                 || isBroadcasting(localBluetoothManager)) {
648             return false;
649         }
650         Set<Integer> connectedGroupIds =
651                 assistantProfile.getAllConnectedDevices().stream()
652                         .map(deviceManager::findDevice)
653                         .filter(Objects::nonNull)
654                         .map(BluetoothUtils::getGroupId)
655                         .collect(Collectors.toSet());
656         Set<Integer> activeGroupIds =
657                 leAudioProfile.getActiveDevices().stream()
658                         .map(deviceManager::findDevice)
659                         .filter(Objects::nonNull)
660                         .map(BluetoothUtils::getGroupId)
661                         .collect(Collectors.toSet());
662         int groupId = getGroupId(cachedDevice);
663         return activeGroupIds.size() == 1
664                 && !activeGroupIds.contains(groupId)
665                 && connectedGroupIds.size() == 2
666                 && connectedGroupIds.contains(groupId);
667     }
668 
669     /** Returns if the le audio sharing UI is available. */
isAudioSharingUIAvailable(@ullable Context context)670     public static boolean isAudioSharingUIAvailable(@Nullable Context context) {
671         return (Flags.enableLeAudioSharing()
672                 || (context != null && Flags.audioSharingDeveloperOption()
673                 && getAudioSharingPreviewValue(context.getContentResolver())))
674                 && isAudioSharingSupported();
675     }
676 
677     /** Returns if the le audio sharing hysteresis mode fix is available. */
678     @WorkerThread
isAudioSharingHysteresisModeFixAvailable(@ullable Context context)679     public static boolean isAudioSharingHysteresisModeFixAvailable(@Nullable Context context) {
680         return (audioSharingHysteresisModeFix() && Flags.enableLeAudioSharing())
681                 || (context != null && Flags.audioSharingDeveloperOption()
682                 && getAudioSharingPreviewValue(context.getContentResolver()));
683     }
684 
685     /** Returns if the le audio sharing is enabled. */
isAudioSharingEnabled()686     public static boolean isAudioSharingEnabled() {
687         return Flags.enableLeAudioSharing() && isAudioSharingSupported();
688     }
689 
690     /** Returns if the le audio sharing preview is enabled in developer option. */
isAudioSharingPreviewEnabled(@ullable ContentResolver contentResolver)691     public static boolean isAudioSharingPreviewEnabled(@Nullable ContentResolver contentResolver) {
692         return Flags.audioSharingDeveloperOption()
693                 && getAudioSharingPreviewValue(contentResolver)
694                 && isAudioSharingSupported();
695     }
696 
697     /** Returns if the device has le audio sharing capability */
isAudioSharingSupported()698     private static boolean isAudioSharingSupported() {
699         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
700         try {
701             // b/381777424 The APIs have to return ERROR_BLUETOOTH_NOT_ENABLED when BT off based on
702             // CDD definition.
703             // However, app layer need to gate the feature based on whether the device has audio
704             // sharing capability regardless of the BT state.
705             // So here we check the BluetoothProperties when BT off.
706             //
707             // TODO: Also check SystemProperties "persist.bluetooth.leaudio_dynamic_switcher.mode"
708             // and return true if it is in broadcast mode.
709             // Now SystemUI don't have access to read the value.
710             int sourceSupportedCode = adapter.isLeAudioBroadcastSourceSupported();
711             int assistantSupportedCode = adapter.isLeAudioBroadcastAssistantSupported();
712             return (sourceSupportedCode == BluetoothStatusCodes.FEATURE_SUPPORTED
713                     || (sourceSupportedCode == BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED
714                     && BluetoothProperties.isProfileBapBroadcastSourceEnabled().orElse(false)))
715                     && (assistantSupportedCode == BluetoothStatusCodes.FEATURE_SUPPORTED
716                     || (assistantSupportedCode == BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED
717                     && BluetoothProperties.isProfileBapBroadcastAssistEnabled().orElse(false)));
718         } catch (IllegalStateException e) {
719             Log.d(TAG, "Fail to check isAudioSharingSupported, e = ", e);
720             return false;
721         }
722     }
723 
724     /** Check if the {@link CachedBluetoothDevice} is a media device */
725     @WorkerThread
isMediaDevice(@ullable CachedBluetoothDevice cachedDevice)726     public static boolean isMediaDevice(@Nullable CachedBluetoothDevice cachedDevice) {
727         if (cachedDevice == null) return false;
728         return cachedDevice.getProfiles().stream()
729                 .anyMatch(
730                         profile ->
731                                 profile instanceof A2dpProfile
732                                         || profile instanceof HearingAidProfile
733                                         || profile instanceof LeAudioProfile
734                                         || profile instanceof HeadsetProfile);
735     }
736 
737     /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */
738     @WorkerThread
isLeAudioSupported(@ullable CachedBluetoothDevice cachedDevice)739     public static boolean isLeAudioSupported(@Nullable CachedBluetoothDevice cachedDevice) {
740         if (cachedDevice == null) return false;
741         return cachedDevice.getProfiles().stream()
742                 .anyMatch(
743                         profile ->
744                                 profile instanceof LeAudioProfile
745                                         && profile.isEnabled(cachedDevice.getDevice()));
746     }
747 
748     /** Returns if the broadcast is on-going. */
749     @WorkerThread
isBroadcasting(@ullable LocalBluetoothManager manager)750     public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
751         if (manager == null) return false;
752         LocalBluetoothLeBroadcast broadcast =
753                 manager.getProfileManager().getLeAudioBroadcastProfile();
754         return broadcast != null && broadcast.isEnabled(null);
755     }
756 
757     /**
758      * Check if {@link CachedBluetoothDevice} (lead or member) has connected to a broadcast source.
759      *
760      * @param cachedDevice The cached bluetooth device to check.
761      * @param localBtManager The BT manager to provide BT functions.
762      * @return Whether the device has connected to a broadcast source.
763      */
764     @WorkerThread
hasConnectedBroadcastSource( @ullable CachedBluetoothDevice cachedDevice, @Nullable LocalBluetoothManager localBtManager)765     public static boolean hasConnectedBroadcastSource(
766             @Nullable CachedBluetoothDevice cachedDevice,
767             @Nullable LocalBluetoothManager localBtManager) {
768         if (cachedDevice == null) return false;
769         if (hasConnectedBroadcastSourceForBtDevice(cachedDevice.getDevice(), localBtManager)) {
770             Log.d(
771                     TAG,
772                     "Lead device has connected broadcast source, device = "
773                             + cachedDevice.getDevice().getAnonymizedAddress());
774             return true;
775         }
776         // Return true if member device is in broadcast.
777         for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
778             if (hasConnectedBroadcastSourceForBtDevice(device.getDevice(), localBtManager)) {
779                 Log.d(
780                         TAG,
781                         "Member device has connected broadcast source, device = "
782                                 + device.getDevice().getAnonymizedAddress());
783                 return true;
784             }
785         }
786         return false;
787     }
788 
789     /**
790      * Check if {@link BluetoothDevice} has connected to a broadcast source.
791      *
792      * @param device The bluetooth device to check.
793      * @param localBtManager The BT manager to provide BT functions.
794      * @return Whether the device has connected to a broadcast source.
795      */
796     @WorkerThread
hasConnectedBroadcastSourceForBtDevice( @ullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager)797     public static boolean hasConnectedBroadcastSourceForBtDevice(
798             @Nullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager) {
799         if (localBtManager == null) {
800             Log.d(TAG, "Skip check hasConnectedBroadcastSourceForBtDevice due to arg is null");
801             return false;
802         }
803         if (isAudioSharingHysteresisModeFixAvailable(localBtManager.getContext())) {
804             return hasActiveLocalBroadcastSourceForBtDevice(device, localBtManager);
805         }
806         LocalBluetoothLeBroadcastAssistant assistant =
807                 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
808         if (device == null || assistant == null) {
809             Log.d(TAG, "Skip check hasConnectedBroadcastSourceForBtDevice due to arg is null");
810             return false;
811         }
812         List<BluetoothLeBroadcastReceiveState> sourceList = assistant.getAllSources(device);
813         return !sourceList.isEmpty() && sourceList.stream().anyMatch(BluetoothUtils::isConnected);
814     }
815 
816     /**
817      * Check if {@link BluetoothDevice} has a active local broadcast source.
818      *
819      * @param device The bluetooth device to check.
820      * @param localBtManager The BT manager to provide BT functions.
821      * @return Whether the device has a active local broadcast source.
822      */
823     @WorkerThread
hasActiveLocalBroadcastSourceForBtDevice( @ullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager)824     public static boolean hasActiveLocalBroadcastSourceForBtDevice(
825             @Nullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager) {
826         LocalBluetoothLeBroadcastAssistant assistant =
827                 localBtManager == null
828                         ? null
829                         : localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
830         LocalBluetoothLeBroadcast broadcast =
831                 localBtManager == null
832                         ? null
833                         : localBtManager.getProfileManager().getLeAudioBroadcastProfile();
834         if (device == null || assistant == null || broadcast == null) {
835             Log.d(TAG, "Skip check hasActiveLocalBroadcastSourceForBtDevice due to arg is null");
836             return false;
837         }
838         List<BluetoothLeBroadcastReceiveState> sourceList = assistant.getAllSources(device);
839         int broadcastId = broadcast.getLatestBroadcastId();
840         return !sourceList.isEmpty()
841                 && broadcastId != UNKNOWN_VALUE_PLACEHOLDER
842                 && sourceList.stream().anyMatch(source -> isSourceMatched(source, broadcastId));
843     }
844 
845     /** Checks the connectivity status based on the provided broadcast receive state. */
846     @WorkerThread
isConnected(BluetoothLeBroadcastReceiveState state)847     public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
848         return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0);
849     }
850 
851     /** Checks if the broadcast id is matched based on the provided broadcast receive state. */
852     @WorkerThread
isSourceMatched( @ullable BluetoothLeBroadcastReceiveState state, int broadcastId)853     public static boolean isSourceMatched(
854             @Nullable BluetoothLeBroadcastReceiveState state, int broadcastId) {
855         return state != null && state.getBroadcastId() == broadcastId;
856     }
857 
858     /**
859      * Checks if the Bluetooth device is an available hearing device, which means: 1) currently
860      * connected 2) is Hearing Aid 3) connected profile match hearing aid related profiles (e.g.
861      * ASHA, HAP)
862      *
863      * @param cachedDevice the CachedBluetoothDevice
864      * @return if the device is Available hearing device
865      */
866     @WorkerThread
isAvailableHearingDevice(CachedBluetoothDevice cachedDevice)867     public static boolean isAvailableHearingDevice(CachedBluetoothDevice cachedDevice) {
868         if (isDeviceConnected(cachedDevice) && cachedDevice.isConnectedHearingAidDevice()) {
869             Log.d(
870                     TAG,
871                     "isFilterMatched() device : "
872                             + cachedDevice.getName()
873                             + ", the profile is connected.");
874             return true;
875         }
876         return false;
877     }
878 
879     /**
880      * Check if the Bluetooth device is a ConnectedBluetoothDevice, which means: 1) currently
881      * connected 2) is not Hearing Aid or LE Audio AND 3) connected profile does not match
882      * currentAudioProfile
883      *
884      * @param cachedDevice the CachedBluetoothDevice
885      * @param isOngoingCall get the current audio profile based on if in phone call
886      * @return if the device is AvailableMediaBluetoothDevice
887      */
888     @WorkerThread
isConnectedBluetoothDevice( CachedBluetoothDevice cachedDevice, boolean isOngoingCall)889     public static boolean isConnectedBluetoothDevice(
890             CachedBluetoothDevice cachedDevice, boolean isOngoingCall) {
891         int currentAudioProfile;
892 
893         if (isOngoingCall) {
894             currentAudioProfile = BluetoothProfile.HEADSET;
895         } else {
896             currentAudioProfile = BluetoothProfile.A2DP;
897         }
898 
899         boolean isFilterMatched = false;
900         if (isDeviceConnected(cachedDevice)) {
901             // If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP.
902             // It would not show in Connected Devices group.
903             if (cachedDevice.isConnectedAshaHearingAidDevice()
904                     || cachedDevice.isConnectedLeAudioDevice()) {
905                 return false;
906             }
907             // According to the current audio profile type,
908             // this page will show the bluetooth device that doesn't have corresponding profile.
909             // For example:
910             // If current audio profile is a2dp,
911             // show the bluetooth device that doesn't have a2dp profile.
912             // If current audio profile is headset,
913             // show the bluetooth device that doesn't have headset profile.
914             switch (currentAudioProfile) {
915                 case BluetoothProfile.A2DP:
916                     isFilterMatched = !cachedDevice.isConnectedA2dpDevice();
917                     break;
918                 case BluetoothProfile.HEADSET:
919                     isFilterMatched = !cachedDevice.isConnectedHfpDevice();
920                     break;
921             }
922         }
923         return isFilterMatched;
924     }
925 
926     /**
927      * Check if the Bluetooth device is an active media device
928      *
929      * @param cachedDevice the CachedBluetoothDevice
930      * @return if the Bluetooth device is an active media device
931      */
isActiveMediaDevice(CachedBluetoothDevice cachedDevice)932     public static boolean isActiveMediaDevice(CachedBluetoothDevice cachedDevice) {
933         return cachedDevice.isActiveDevice(BluetoothProfile.A2DP)
934                 || cachedDevice.isActiveDevice(BluetoothProfile.HEADSET)
935                 || cachedDevice.isActiveDevice(BluetoothProfile.HEARING_AID)
936                 || cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
937     }
938 
939     /**
940      * Check if the Bluetooth device is an active LE Audio device
941      *
942      * @param cachedDevice the CachedBluetoothDevice
943      * @return if the Bluetooth device is an active LE Audio device
944      */
isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice)945     public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) {
946         return cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
947     }
948 
isDeviceConnected(CachedBluetoothDevice cachedDevice)949     private static boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) {
950         if (cachedDevice == null) {
951             return false;
952         }
953         final BluetoothDevice device = cachedDevice.getDevice();
954         return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected();
955     }
956 
957     @SuppressLint("NewApi") // Hidden API made public
doesClassMatch(BluetoothClass btClass, int classId)958     private static boolean doesClassMatch(BluetoothClass btClass, int classId) {
959         return btClass.doesClassMatch(classId);
960     }
961 
extraTagValue(String tag, String metaData)962     private static String extraTagValue(String tag, String metaData) {
963         if (TextUtils.isEmpty(metaData)) {
964             return null;
965         }
966         Pattern pattern = Pattern.compile(generateExpressionWithTag(tag, "(.*?)"));
967         Matcher matcher = pattern.matcher(metaData);
968         if (matcher.find()) {
969             return matcher.group(1);
970         }
971         return null;
972     }
973 
getTagStart(String tag)974     private static String getTagStart(String tag) {
975         return String.format(Locale.ENGLISH, "<%s>", tag);
976     }
977 
getTagEnd(String tag)978     private static String getTagEnd(String tag) {
979         return String.format(Locale.ENGLISH, "</%s>", tag);
980     }
981 
generateExpressionWithTag(String tag, String value)982     private static String generateExpressionWithTag(String tag, String value) {
983         return getTagStart(tag) + value + getTagEnd(tag);
984     }
985 
986     /**
987      * Returns the BluetoothDevice's exclusive manager ({@link
988      * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists, otherwise null.
989      */
990     @Nullable
getExclusiveManager(BluetoothDevice bluetoothDevice)991     private static String getExclusiveManager(BluetoothDevice bluetoothDevice) {
992         byte[] exclusiveManagerBytes =
993                 bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER);
994         if (exclusiveManagerBytes == null) {
995             Log.d(
996                     TAG,
997                     "Bluetooth device "
998                             + bluetoothDevice.getName()
999                             + " doesn't have exclusive manager");
1000             return null;
1001         }
1002         return new String(exclusiveManagerBytes);
1003     }
1004 
1005     /** Checks if given package is installed and enabled */
isPackageInstalledAndEnabled(Context context, String packageName)1006     private static boolean isPackageInstalledAndEnabled(Context context, String packageName) {
1007         PackageManager packageManager = context.getPackageManager();
1008         try {
1009             ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0);
1010             return appInfo.enabled;
1011         } catch (PackageManager.NameNotFoundException e) {
1012             Log.d(TAG, "Package " + packageName + " is not installed/enabled");
1013         }
1014         return false;
1015     }
1016 
1017     /**
1018      * A BluetoothDevice is exclusively managed if 1) it has field {@link
1019      * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata. 2) the exclusive manager app is
1020      * installed and enabled.
1021      */
isExclusivelyManagedBluetoothDevice( @onNull Context context, @NonNull BluetoothDevice bluetoothDevice)1022     public static boolean isExclusivelyManagedBluetoothDevice(
1023             @NonNull Context context, @NonNull BluetoothDevice bluetoothDevice) {
1024         String exclusiveManagerName = getExclusiveManager(bluetoothDevice);
1025         if (exclusiveManagerName == null) {
1026             return false;
1027         }
1028 
1029         ComponentName exclusiveManagerComponent =
1030                 ComponentName.unflattenFromString(exclusiveManagerName);
1031         String exclusiveManagerPackage =
1032                 exclusiveManagerComponent != null
1033                         ? exclusiveManagerComponent.getPackageName()
1034                         : exclusiveManagerName;
1035 
1036         if (!isPackageInstalledAndEnabled(context, exclusiveManagerPackage)) {
1037             return false;
1038         } else {
1039             Log.d(TAG, "Found exclusively managed app " + exclusiveManagerPackage);
1040             return true;
1041         }
1042     }
1043 
1044     /**
1045      * Get CSIP group id for {@link CachedBluetoothDevice}.
1046      *
1047      * <p>If CachedBluetoothDevice#getGroupId is invalid, fetch group id from
1048      * LeAudioProfile#getGroupId.
1049      */
getGroupId(@ullable CachedBluetoothDevice cachedDevice)1050     public static int getGroupId(@Nullable CachedBluetoothDevice cachedDevice) {
1051         if (cachedDevice == null) return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
1052         int groupId = cachedDevice.getGroupId();
1053         String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress();
1054         if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
1055             Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress);
1056             return groupId;
1057         }
1058         for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
1059             if (profile instanceof LeAudioProfile) {
1060                 Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress);
1061                 return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice());
1062             }
1063         }
1064         Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress);
1065         return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
1066     }
1067 
1068     /** Get primary device Uri in broadcast. */
1069     @NonNull
getPrimaryGroupIdUriForBroadcast()1070     public static String getPrimaryGroupIdUriForBroadcast() {
1071         // TODO: once API is stable, deprecate SettingsProvider solution
1072         return "bluetooth_le_broadcast_fallback_active_group_id";
1073     }
1074 
1075     /** Get primary device group id in broadcast from SettingsProvider. */
1076     @WorkerThread
getPrimaryGroupIdForBroadcast(@onNull ContentResolver contentResolver)1077     public static int getPrimaryGroupIdForBroadcast(@NonNull ContentResolver contentResolver) {
1078         // TODO: once API is stable, deprecate SettingsProvider solution
1079         return Settings.Secure.getInt(
1080                 contentResolver,
1081                 getPrimaryGroupIdUriForBroadcast(),
1082                 BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
1083     }
1084 
1085     /**
1086      * Get primary device group id in broadcast.
1087      *
1088      * If Flags.adoptPrimaryGroupManagementApiV2 is enabled, get group id by API,
1089      * Otherwise, still get value from SettingsProvider.
1090      */
1091     @WorkerThread
getPrimaryGroupIdForBroadcast(@onNull ContentResolver contentResolver, @Nullable LocalBluetoothManager manager)1092     public static int getPrimaryGroupIdForBroadcast(@NonNull ContentResolver contentResolver,
1093             @Nullable LocalBluetoothManager manager) {
1094         if (Flags.adoptPrimaryGroupManagementApiV2()) {
1095             LeAudioProfile leaProfile = manager == null ? null :
1096                     manager.getProfileManager().getLeAudioProfile();
1097             if (leaProfile == null) {
1098                 Log.d(TAG, "getPrimaryGroupIdForBroadcast: profile is null");
1099                 return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
1100             }
1101             return leaProfile.getBroadcastToUnicastFallbackGroup();
1102         } else {
1103             return getPrimaryGroupIdForBroadcast(contentResolver);
1104         }
1105     }
1106 
1107     /** Get develop option value for audio sharing preview. */
1108     @WorkerThread
getAudioSharingPreviewValue(@ullable ContentResolver contentResolver)1109     public static boolean getAudioSharingPreviewValue(@Nullable ContentResolver contentResolver) {
1110         if (contentResolver == null) return false;
1111         return Settings.Global.getInt(
1112                 contentResolver,
1113                 DEVELOPER_OPTION_PREVIEW_KEY,
1114                 0 // value off
1115         ) == 1;
1116     }
1117 
1118     /** Get secondary {@link CachedBluetoothDevice} in broadcast. */
1119     @Nullable
1120     @WorkerThread
getSecondaryDeviceForBroadcast( @onNull ContentResolver contentResolver, @Nullable LocalBluetoothManager localBtManager)1121     public static CachedBluetoothDevice getSecondaryDeviceForBroadcast(
1122             @NonNull ContentResolver contentResolver,
1123             @Nullable LocalBluetoothManager localBtManager) {
1124         if (localBtManager == null) return null;
1125         LocalBluetoothLeBroadcast broadcast =
1126                 localBtManager.getProfileManager().getLeAudioBroadcastProfile();
1127         if (broadcast == null || !broadcast.isEnabled(null)) return null;
1128         int primaryGroupId = getPrimaryGroupIdForBroadcast(contentResolver, localBtManager);
1129         if (primaryGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) return null;
1130         LocalBluetoothLeBroadcastAssistant assistant =
1131                 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
1132         CachedBluetoothDeviceManager deviceManager = localBtManager.getCachedDeviceManager();
1133         List<BluetoothDevice> devices = assistant.getAllConnectedDevices();
1134         for (BluetoothDevice device : devices) {
1135             CachedBluetoothDevice cachedDevice = deviceManager.findDevice(device);
1136             if (hasConnectedBroadcastSource(cachedDevice, localBtManager)) {
1137                 int groupId = getGroupId(cachedDevice);
1138                 if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
1139                         && groupId != primaryGroupId) {
1140                     return cachedDevice;
1141                 }
1142             }
1143         }
1144         return null;
1145     }
1146 
1147     /**
1148      * Gets {@link AudioDeviceAttributes} of bluetooth device for spatial audio. Returns null if
1149      * it's not an audio device(no A2DP, LE Audio and Hearing Aid profile).
1150      */
1151     @Nullable
getAudioDeviceAttributesForSpatialAudio( CachedBluetoothDevice cachedDevice, @AudioManager.AudioDeviceCategory int audioDeviceCategory)1152     public static AudioDeviceAttributes getAudioDeviceAttributesForSpatialAudio(
1153             CachedBluetoothDevice cachedDevice,
1154             @AudioManager.AudioDeviceCategory int audioDeviceCategory) {
1155         AudioDeviceAttributes saDevice = null;
1156         for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
1157             // pick first enabled profile that is compatible with spatial audio
1158             if (SA_PROFILES.contains(profile.getProfileId())
1159                     && profile.isEnabled(cachedDevice.getDevice())) {
1160                 switch (profile.getProfileId()) {
1161                     case BluetoothProfile.A2DP:
1162                         saDevice =
1163                                 new AudioDeviceAttributes(
1164                                         AudioDeviceAttributes.ROLE_OUTPUT,
1165                                         AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
1166                                         cachedDevice.getAddress());
1167                         break;
1168                     case BluetoothProfile.LE_AUDIO:
1169                         if (audioDeviceCategory == AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER) {
1170                             saDevice =
1171                                     new AudioDeviceAttributes(
1172                                             AudioDeviceAttributes.ROLE_OUTPUT,
1173                                             AudioDeviceInfo.TYPE_BLE_SPEAKER,
1174                                             cachedDevice.getAddress());
1175                         } else {
1176                             saDevice =
1177                                     new AudioDeviceAttributes(
1178                                             AudioDeviceAttributes.ROLE_OUTPUT,
1179                                             AudioDeviceInfo.TYPE_BLE_HEADSET,
1180                                             cachedDevice.getAddress());
1181                         }
1182 
1183                         break;
1184                     case BluetoothProfile.HEARING_AID:
1185                         saDevice =
1186                                 new AudioDeviceAttributes(
1187                                         AudioDeviceAttributes.ROLE_OUTPUT,
1188                                         AudioDeviceInfo.TYPE_HEARING_AID,
1189                                         cachedDevice.getAddress());
1190                         break;
1191                     default:
1192                         Log.i(
1193                                 TAG,
1194                                 "unrecognized profile for spatial audio: "
1195                                         + profile.getProfileId());
1196                         break;
1197                 }
1198                 break;
1199             }
1200         }
1201         return saDevice;
1202     }
1203 
1204     /**
1205      * Verifies if the device is temporary bond in audio sharing.
1206      *
1207      * @param bluetoothDevice the BluetoothDevice to verify
1208      * @return if the device is temporary bond
1209      */
isTemporaryBondDevice(@ullable BluetoothDevice bluetoothDevice)1210     public static boolean isTemporaryBondDevice(@Nullable BluetoothDevice bluetoothDevice) {
1211         String metadataValue = getFastPairCustomizedField(bluetoothDevice, TEMP_BOND_TYPE);
1212         return Objects.equals(metadataValue, TEMP_BOND_DEVICE_METADATA_VALUE);
1213     }
1214 
1215     /**
1216      * Set temp bond metadata to device
1217      *
1218      * @param device the BluetoothDevice to be marked as temp bond
1219      *
1220      * Note: It is a workaround since Bluetooth API is not ready.
1221      *       Avoid using this method if possible
1222      */
setTemporaryBondMetadata(@ullable BluetoothDevice device)1223     public static void setTemporaryBondMetadata(@Nullable BluetoothDevice device) {
1224         if (device == null) return;
1225         if (!Flags.enableTemporaryBondDevicesUi()) {
1226             Log.d(TAG, "Skip setTemporaryBondMetadata, flag is disabled");
1227             return;
1228         }
1229         String fastPairCustomizedMeta = getStringMetaData(device,
1230                 METADATA_FAST_PAIR_CUSTOMIZED_FIELDS);
1231         String fullContentWithTag = generateExpressionWithTag(TEMP_BOND_TYPE,
1232                 TEMP_BOND_DEVICE_METADATA_VALUE);
1233         if (TextUtils.isEmpty(fastPairCustomizedMeta)) {
1234             fastPairCustomizedMeta = fullContentWithTag;
1235         } else {
1236             String oldValue = extraTagValue(TEMP_BOND_TYPE, fastPairCustomizedMeta);
1237             if (TextUtils.isEmpty(oldValue)) {
1238                 fastPairCustomizedMeta += fullContentWithTag;
1239             } else {
1240                 fastPairCustomizedMeta =
1241                         fastPairCustomizedMeta.replace(
1242                                 generateExpressionWithTag(TEMP_BOND_TYPE, oldValue),
1243                                 fullContentWithTag);
1244             }
1245         }
1246         device.setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, fastPairCustomizedMeta.getBytes());
1247     }
1248 
1249     /**
1250      * Returns the {@link InputDevice} of the given bluetooth address if the device is a input
1251      * device.
1252      *
1253      * @param address The address of the bluetooth device
1254      * @return The {@link InputDevice} of the given address if applicable
1255      */
1256     @Nullable
getInputDevice(Context context, String address)1257     public static InputDevice getInputDevice(Context context, String address) {
1258         InputManager im = context.getSystemService(InputManager.class);
1259 
1260         if (im != null) {
1261             for (int deviceId : im.getInputDeviceIds()) {
1262                 String btAddress = im.getInputDeviceBluetoothAddress(deviceId);
1263 
1264                 if (btAddress != null && btAddress.equals(address)) {
1265                     return im.getInputDevice(deviceId);
1266                 }
1267             }
1268         }
1269         return null;
1270     }
1271 
1272     /**
1273      * Identifies whether a device is a stylus using the associated {@link InputDevice} or
1274      * {@link CachedBluetoothDevice}.
1275      * InputDevices are only available when the device is USI or Bluetooth-connected, whereas
1276      * CachedBluetoothDevices are available for Bluetooth devices when connected or paired,
1277      * so to handle all cases, both are needed.
1278      *
1279      * @param inputDevice           The associated input device of the stylus
1280      * @param cachedBluetoothDevice The associated bluetooth device of the stylus
1281      */
isDeviceStylus(@ullable InputDevice inputDevice, @Nullable CachedBluetoothDevice cachedBluetoothDevice)1282     public static boolean isDeviceStylus(@Nullable InputDevice inputDevice,
1283             @Nullable CachedBluetoothDevice cachedBluetoothDevice) {
1284         if (inputDevice != null && inputDevice.supportsSource(InputDevice.SOURCE_STYLUS)) {
1285             return true;
1286         }
1287 
1288         if (cachedBluetoothDevice != null) {
1289             BluetoothDevice bluetoothDevice = cachedBluetoothDevice.getDevice();
1290             String deviceType = BluetoothUtils.getStringMetaData(bluetoothDevice,
1291                     BluetoothDevice.METADATA_DEVICE_TYPE);
1292             return TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_STYLUS);
1293         }
1294 
1295         return false;
1296     }
1297 
1298     /** Gets key missing count of the device. This is a workaround before the API is rolled out. */
getKeyMissingCount(BluetoothDevice device)1299     public static Integer getKeyMissingCount(BluetoothDevice device) {
1300         try {
1301             Method m = BluetoothDevice.class.getDeclaredMethod("getKeyMissingCount");
1302             return (int) m.invoke(device);
1303         } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
1304             Log.w(TAG, "error happens when getKeyMissingCount.");
1305             return null;
1306         }
1307     }
1308 }
1309