1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.bluetooth;
18 
19 import static android.bluetooth.BluetoothDevice.METADATA_MODEL_NAME;
20 
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothProfile;
23 import android.content.Context;
24 import android.os.SystemProperties;
25 import android.provider.DeviceConfig;
26 import android.sysprop.BluetoothProperties;
27 import android.text.TextUtils;
28 import android.util.Log;
29 
30 import androidx.annotation.VisibleForTesting;
31 import androidx.preference.Preference;
32 import androidx.preference.PreferenceCategory;
33 import androidx.preference.PreferenceFragmentCompat;
34 import androidx.preference.PreferenceScreen;
35 import androidx.preference.SwitchPreferenceCompat;
36 import androidx.preference.TwoStatePreference;
37 
38 import com.android.settings.R;
39 import com.android.settings.core.SettingsUIDeviceConfig;
40 import com.android.settings.flags.Flags;
41 import com.android.settings.overlay.FeatureFactory;
42 import com.android.settingslib.bluetooth.A2dpProfile;
43 import com.android.settingslib.bluetooth.BluetoothUtils;
44 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
45 import com.android.settingslib.bluetooth.HeadsetProfile;
46 import com.android.settingslib.bluetooth.HearingAidProfile;
47 import com.android.settingslib.bluetooth.LeAudioProfile;
48 import com.android.settingslib.bluetooth.LocalBluetoothManager;
49 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
50 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
51 import com.android.settingslib.bluetooth.MapProfile;
52 import com.android.settingslib.bluetooth.PanProfile;
53 import com.android.settingslib.bluetooth.PbapServerProfile;
54 import com.android.settingslib.core.lifecycle.Lifecycle;
55 import com.android.settingslib.utils.ThreadUtils;
56 
57 import java.util.ArrayList;
58 import java.util.HashMap;
59 import java.util.List;
60 import java.util.Map;
61 import java.util.Set;
62 import java.util.concurrent.atomic.AtomicReference;
63 
64 /**
65  * This class adds switches for toggling the individual profiles that a Bluetooth device
66  * supports, such as "Phone audio", "Media audio", "Contact sharing", etc.
67  */
68 public class BluetoothDetailsProfilesController extends BluetoothDetailsController
69         implements Preference.OnPreferenceClickListener,
70         LocalBluetoothProfileManager.ServiceListener {
71     private static final String TAG = "BtDetailsProfilesCtrl";
72 
73     private static final String KEY_PROFILES_GROUP = "bluetooth_profiles";
74     private static final String KEY_BOTTOM_PREFERENCE = "bottom_preference";
75     private static final int ORDINAL = 99;
76 
77     @VisibleForTesting
78     static final String HIGH_QUALITY_AUDIO_PREF_TAG = "A2dpProfileHighQualityAudio";
79 
80     private static final String ENABLE_DUAL_MODE_AUDIO =
81             "persist.bluetooth.enable_dual_mode_audio";
82     private static final String LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY =
83             "ro.bluetooth.leaudio.le_audio_connection_by_default";
84     private static final boolean LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE = true;
85     private static final String LE_AUDIO_TOGGLE_VISIBLE_PROPERTY =
86             "persist.bluetooth.leaudio.toggle_visible";
87 
88     private final AtomicReference<Set<String>> mInvisiblePreferenceKey = new AtomicReference<>();
89 
90     private LocalBluetoothManager mManager;
91     private LocalBluetoothProfileManager mProfileManager;
92     private CachedBluetoothDevice mCachedDevice;
93     private List<CachedBluetoothDevice> mAllOfCachedDevices;
94     private Map<String, List<CachedBluetoothDevice>> mProfileDeviceMap =
95             new HashMap<String, List<CachedBluetoothDevice>>();
96     private boolean mIsLeContactSharingEnabled = false;
97     private boolean mIsLeAudioToggleEnabled = false;
98 
99     @VisibleForTesting
100     PreferenceCategory mProfilesContainer;
101 
BluetoothDetailsProfilesController(Context context, PreferenceFragmentCompat fragment, LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle)102     public BluetoothDetailsProfilesController(Context context, PreferenceFragmentCompat fragment,
103             LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle) {
104         super(context, fragment, device, lifecycle);
105         mManager = manager;
106         mProfileManager = mManager.getProfileManager();
107         mCachedDevice = device;
108         mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
109         lifecycle.addObserver(this);
110     }
111 
112     @Override
init(PreferenceScreen screen)113     protected void init(PreferenceScreen screen) {
114         mProfilesContainer = (PreferenceCategory)screen.findPreference(getPreferenceKey());
115         mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
116         // Call refresh here even though it will get called later in onResume, to avoid the
117         // list of switches appearing to "pop" into the page.
118         refresh();
119     }
120 
121     /**
122      * Creates a switch preference for the particular profile.
123      *
124      * @param context The context to use when creating the TwoStatePreference
125      * @param profile The profile for which the preference controls.
126      * @return A preference that allows the user to choose whether this profile
127      * will be connected to.
128      */
createProfilePreference(Context context, LocalBluetoothProfile profile)129     private TwoStatePreference createProfilePreference(Context context,
130             LocalBluetoothProfile profile) {
131         TwoStatePreference pref = new SwitchPreferenceCompat(context);
132         pref.setKey(profile.toString());
133         pref.setTitle(profile.getNameResource(mCachedDevice.getDevice()));
134         pref.setOnPreferenceClickListener(this);
135         pref.setOrder(profile.getOrdinal());
136 
137         boolean isLeEnabledByDefault =
138                 SystemProperties.getBoolean(LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY, true);
139 
140         if (profile instanceof LeAudioProfile && (!isLeEnabledByDefault || !isModelNameInAllowList(
141                 BluetoothUtils.getStringMetaData(mCachedDevice.getDevice(),
142                         METADATA_MODEL_NAME)))) {
143             pref.setSummary(R.string.device_details_leaudio_toggle_summary);
144         }
145         return pref;
146     }
147 
148     /**
149      * Checks if the device model name is in the LE audio allow list based on its model name.
150      *
151      * @param modelName The model name of the device to be checked.
152      * @return true if the device is in the allow list, false otherwise.
153      */
154     @VisibleForTesting
isModelNameInAllowList(String modelName)155     boolean isModelNameInAllowList(String modelName) {
156         if (modelName == null || modelName.isEmpty()) {
157             return false;
158         }
159         return BluetoothProperties.le_audio_allow_list().contains(modelName);
160     }
161 
162     /**
163      * Refreshes the state for an existing TwoStatePreference for a profile.
164      */
refreshProfilePreference(TwoStatePreference profilePref, LocalBluetoothProfile profile)165     private void refreshProfilePreference(TwoStatePreference profilePref,
166             LocalBluetoothProfile profile) {
167         BluetoothDevice device = mCachedDevice.getDevice();
168         boolean isLeAudioEnabled = isLeAudioEnabled();
169         if (profile instanceof A2dpProfile || profile instanceof HeadsetProfile
170                 || profile instanceof LeAudioProfile) {
171             List<CachedBluetoothDevice> deviceList = mProfileDeviceMap.get(
172                     profile.toString());
173             boolean isBusy = deviceList != null
174                     && deviceList.stream().anyMatch(item -> item.isBusy());
175             profilePref.setEnabled(!isBusy);
176         } else if (profile instanceof PbapServerProfile
177                 && isLeAudioEnabled
178                 && !mIsLeContactSharingEnabled) {
179             profilePref.setEnabled(false);
180         } else {
181             profilePref.setEnabled(!mCachedDevice.isBusy());
182         }
183 
184         if (profile instanceof LeAudioProfile) {
185             profilePref.setVisible(mIsLeAudioToggleEnabled);
186         }
187 
188         if (profile instanceof MapProfile) {
189             profilePref.setChecked(device.getMessageAccessPermission()
190                     == BluetoothDevice.ACCESS_ALLOWED);
191         } else if (profile instanceof PbapServerProfile) {
192             profilePref.setChecked(device.getPhonebookAccessPermission()
193                     == BluetoothDevice.ACCESS_ALLOWED);
194             profilePref.setSummary(profile.getSummaryResourceForDevice(mCachedDevice.getDevice()));
195         } else if (profile instanceof PanProfile) {
196             profilePref.setChecked(profile.getConnectionStatus(device) ==
197                     BluetoothProfile.STATE_CONNECTED);
198         } else {
199             profilePref.setChecked(profile.isEnabled(device));
200         }
201 
202         if (profile instanceof A2dpProfile) {
203             A2dpProfile a2dp = (A2dpProfile) profile;
204             TwoStatePreference highQualityPref =
205                     mProfilesContainer.findPreference(HIGH_QUALITY_AUDIO_PREF_TAG);
206             if (highQualityPref != null) {
207                 if (a2dp.isEnabled(device) && a2dp.supportsHighQualityAudio(device)) {
208                     highQualityPref.setVisible(true);
209                     highQualityPref.setTitle(a2dp.getHighQualityAudioOptionLabel(device));
210                     highQualityPref.setChecked(a2dp.isHighQualityAudioEnabled(device));
211                     highQualityPref.setEnabled(!mCachedDevice.isBusy());
212                 } else {
213                     highQualityPref.setVisible(false);
214                 }
215             }
216         }
217     }
218 
isLeAudioEnabled()219     private boolean isLeAudioEnabled(){
220         LocalBluetoothProfile leAudio = mProfileManager.getLeAudioProfile();
221         if (leAudio != null) {
222             List<CachedBluetoothDevice> leAudioDeviceList = mProfileDeviceMap.get(
223                     leAudio.toString());
224             if (leAudioDeviceList != null
225                     && leAudioDeviceList.stream()
226                     .anyMatch(item -> leAudio.isEnabled(item.getDevice()))) {
227                 return true;
228             }
229         }
230         return false;
231     }
232 
233     /**
234      * Helper method to enable a profile for a device.
235      */
enableProfile(LocalBluetoothProfile profile)236     private void enableProfile(LocalBluetoothProfile profile) {
237         final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
238         if (profile instanceof PbapServerProfile) {
239             bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
240             // We don't need to do the additional steps below for this profile.
241             return;
242         }
243         if (profile instanceof MapProfile) {
244             bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
245         }
246 
247         if (profile instanceof LeAudioProfile) {
248             enableLeAudioProfile(profile);
249             return;
250         }
251 
252         profile.setEnabled(bluetoothDevice, true);
253     }
254 
255     /**
256      * Helper method to disable a profile for a device
257      */
disableProfile(LocalBluetoothProfile profile)258     private void disableProfile(LocalBluetoothProfile profile) {
259         if (profile instanceof LeAudioProfile) {
260             disableLeAudioProfile(profile);
261             return;
262         }
263 
264         final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
265         profile.setEnabled(bluetoothDevice, false);
266 
267         if (profile instanceof MapProfile) {
268             bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
269         } else if (profile instanceof PbapServerProfile) {
270             bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
271         }
272     }
273 
274     /**
275      * When the pref for a bluetooth profile is clicked on, we want to toggle the enabled/disabled
276      * state for that profile.
277      */
278     @Override
onPreferenceClick(Preference preference)279     public boolean onPreferenceClick(Preference preference) {
280         LocalBluetoothProfile profile = mProfileManager.getProfileByName(preference.getKey());
281         if (profile == null) {
282             // It might be the PbapServerProfile, which is not stored by name.
283             PbapServerProfile psp = mManager.getProfileManager().getPbapProfile();
284             if (TextUtils.equals(preference.getKey(), psp.toString())) {
285                 profile = psp;
286             } else {
287                 return false;
288             }
289         }
290         TwoStatePreference profilePref = (TwoStatePreference) preference;
291         if (profilePref.isChecked()) {
292             enableProfile(profile);
293         } else {
294             disableProfile(profile);
295         }
296         refreshProfilePreference(profilePref, profile);
297         return true;
298     }
299 
300     /**
301      * Helper to get the list of connectable and special profiles.
302      */
getProfiles()303     private List<LocalBluetoothProfile> getProfiles() {
304         List<LocalBluetoothProfile> result = new ArrayList<LocalBluetoothProfile>();
305         mProfileDeviceMap.clear();
306         if (mAllOfCachedDevices == null || mAllOfCachedDevices.isEmpty()) {
307             return result;
308         }
309         for (CachedBluetoothDevice cachedItem : mAllOfCachedDevices) {
310             List<LocalBluetoothProfile> tmpResult = cachedItem.getConnectableProfiles();
311             for (LocalBluetoothProfile profile : tmpResult) {
312                 if (mProfileDeviceMap.containsKey(profile.toString())) {
313                     mProfileDeviceMap.get(profile.toString()).add(cachedItem);
314                 } else {
315                     List<CachedBluetoothDevice> tmpCachedDeviceList =
316                             new ArrayList<CachedBluetoothDevice>();
317                     tmpCachedDeviceList.add(cachedItem);
318                     mProfileDeviceMap.put(profile.toString(), tmpCachedDeviceList);
319                     result.add(profile);
320                 }
321             }
322         }
323 
324         final BluetoothDevice device = mCachedDevice.getDevice();
325         final int pbapPermission = device.getPhonebookAccessPermission();
326         // Only provide PBAP cabability if the client device has requested PBAP.
327         if (pbapPermission != BluetoothDevice.ACCESS_UNKNOWN) {
328             final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile();
329             result.add(psp);
330         }
331 
332         final MapProfile mapProfile = mManager.getProfileManager().getMapProfile();
333         final int mapPermission = device.getMessageAccessPermission();
334         if (mapPermission != BluetoothDevice.ACCESS_UNKNOWN) {
335             result.add(mapProfile);
336         }
337 
338         // Removes phone calls & media audio toggles for dual mode devices
339         boolean leAudioSupported = result.contains(
340                 mManager.getProfileManager().getLeAudioProfile());
341         boolean classicAudioSupported = result.contains(
342                 mManager.getProfileManager().getA2dpProfile()) || result.contains(
343                 mManager.getProfileManager().getHeadsetProfile());
344         if (leAudioSupported && classicAudioSupported) {
345             result.remove(mManager.getProfileManager().getA2dpProfile());
346             result.remove(mManager.getProfileManager().getHeadsetProfile());
347         }
348         Log.d(TAG, "getProfiles:Map:" + mProfileDeviceMap);
349         return result;
350     }
351 
352     /**
353      * Disable the Le Audio profile for each of the Le Audio devices.
354      *
355      * @param profile the LeAudio profile
356      */
disableLeAudioProfile(LocalBluetoothProfile profile)357     private void disableLeAudioProfile(LocalBluetoothProfile profile) {
358         if (profile == null || mProfileDeviceMap.get(profile.toString()) == null) {
359             Log.e(TAG, "There is no the LE profile or no device in mProfileDeviceMap. Do nothing.");
360             return;
361         }
362 
363         LocalBluetoothProfile asha = mProfileManager.getHearingAidProfile();
364         LocalBluetoothProfile broadcastAssistant =
365                 mProfileManager.getLeAudioBroadcastAssistantProfile();
366 
367         for (CachedBluetoothDevice leAudioDevice : mProfileDeviceMap.get(profile.toString())) {
368             Log.d(TAG,
369                     "device:" + leAudioDevice.getDevice().getAnonymizedAddress()
370                             + " disable LE profile");
371             profile.setEnabled(leAudioDevice.getDevice(), false);
372             if (asha != null) {
373                 asha.setEnabled(leAudioDevice.getDevice(), true);
374             }
375             if (broadcastAssistant != null) {
376                 Log.d(TAG,
377                         "device:" + leAudioDevice.getDevice().getAnonymizedAddress()
378                                 + " disable LE broadcast assistant profile");
379                 broadcastAssistant.setEnabled(leAudioDevice.getDevice(), false);
380             }
381         }
382 
383         if (!SystemProperties.getBoolean(ENABLE_DUAL_MODE_AUDIO, false)) {
384             Log.i(TAG, "Enabling classic audio profiles because dual mode is disabled");
385             enableProfileAfterUserDisablesLeAudio(mProfileManager.getA2dpProfile());
386             enableProfileAfterUserDisablesLeAudio(mProfileManager.getHeadsetProfile());
387         }
388     }
389 
390     /**
391      * Enable the Le Audio profile for each of the Le Audio devices.
392      *
393      * @param profile the LeAudio profile
394      */
enableLeAudioProfile(LocalBluetoothProfile profile)395     private void enableLeAudioProfile(LocalBluetoothProfile profile) {
396         if (profile == null || mProfileDeviceMap.get(profile.toString()) == null) {
397             Log.e(TAG, "There is no the LE profile or no device in mProfileDeviceMap. Do nothing.");
398             return;
399         }
400 
401         if (!SystemProperties.getBoolean(ENABLE_DUAL_MODE_AUDIO, false)) {
402             Log.i(TAG, "Disabling classic audio profiles because dual mode is disabled");
403             disableProfileBeforeUserEnablesLeAudio(mProfileManager.getA2dpProfile());
404             disableProfileBeforeUserEnablesLeAudio(mProfileManager.getHeadsetProfile());
405         }
406         LocalBluetoothProfile asha = mProfileManager.getHearingAidProfile();
407         LocalBluetoothProfile broadcastAssistant =
408                 mProfileManager.getLeAudioBroadcastAssistantProfile();
409 
410         for (CachedBluetoothDevice leAudioDevice : mProfileDeviceMap.get(profile.toString())) {
411             Log.d(TAG,
412                     "device:" + leAudioDevice.getDevice().getAnonymizedAddress()
413                             + " enable LE profile");
414             profile.setEnabled(leAudioDevice.getDevice(), true);
415             if (asha != null) {
416                 asha.setEnabled(leAudioDevice.getDevice(), false);
417             }
418             if (broadcastAssistant != null) {
419                 Log.d(TAG,
420                         "device:" + leAudioDevice.getDevice().getAnonymizedAddress()
421                                 + " enable LE broadcast assistant profile");
422                 broadcastAssistant.setEnabled(leAudioDevice.getDevice(), true);
423             }
424         }
425     }
426 
disableProfileBeforeUserEnablesLeAudio(LocalBluetoothProfile profile)427     private void disableProfileBeforeUserEnablesLeAudio(LocalBluetoothProfile profile) {
428         if (profile != null && mProfileDeviceMap.get(profile.toString()) != null) {
429             Log.d(TAG, "Disable " + profile.toString() + " before user enables LE");
430             for (CachedBluetoothDevice profileDevice : mProfileDeviceMap.get(profile.toString())) {
431                 if (profile.isEnabled(profileDevice.getDevice())) {
432                     Log.d(TAG, "The " + profileDevice.getDevice().getAnonymizedAddress() + ":"
433                             + profile.toString() + " set disable");
434                     profile.setEnabled(profileDevice.getDevice(), false);
435                 } else {
436                     Log.d(TAG, "The " + profileDevice.getDevice().getAnonymizedAddress() + ":"
437                             + profile.toString() + " profile is disabled. Do nothing.");
438                 }
439             }
440         } else {
441             if (profile == null) {
442                 Log.w(TAG, "profile is null");
443             } else {
444                 Log.w(TAG, profile.toString() + " is not in " + mProfileDeviceMap);
445             }
446         }
447     }
448 
enableProfileAfterUserDisablesLeAudio(LocalBluetoothProfile profile)449     private void enableProfileAfterUserDisablesLeAudio(LocalBluetoothProfile profile) {
450         if (profile != null && mProfileDeviceMap.get(profile.toString()) != null) {
451             Log.d(TAG, "enable " + profile.toString() + "after user disables LE");
452             for (CachedBluetoothDevice profileDevice : mProfileDeviceMap.get(profile.toString())) {
453                 if (!profile.isEnabled(profileDevice.getDevice())) {
454                     Log.d(TAG, "The " + profileDevice.getDevice().getAnonymizedAddress() + ":"
455                             + profile.toString() + " set enable");
456                     profile.setEnabled(profileDevice.getDevice(), true);
457                 } else {
458                     Log.d(TAG, "The " + profileDevice.getDevice().getAnonymizedAddress() + ":"
459                             + profile.toString() + " profile is enabled. Do nothing.");
460                 }
461             }
462         } else {
463             if (profile == null) {
464                 Log.w(TAG, "profile is null");
465             } else {
466                 Log.w(TAG, profile.toString() + " is not in " + mProfileDeviceMap);
467             }
468         }
469     }
470 
471     /**
472      * This is a helper method to be called after adding a Preference for a profile. If that
473      * profile happened to be A2dp and the device supports high quality audio, it will add a
474      * separate preference for controlling whether to actually use high quality audio.
475      *
476      * @param profile the profile just added
477      */
maybeAddHighQualityAudioPref(LocalBluetoothProfile profile)478     private void maybeAddHighQualityAudioPref(LocalBluetoothProfile profile) {
479         if (!(profile instanceof A2dpProfile)) {
480             return;
481         }
482         BluetoothDevice device = mCachedDevice.getDevice();
483         A2dpProfile a2dp = (A2dpProfile) profile;
484         if (a2dp.isProfileReady() && a2dp.supportsHighQualityAudio(device)) {
485             TwoStatePreference highQualityAudioPref = new SwitchPreferenceCompat(
486                     mProfilesContainer.getContext());
487             highQualityAudioPref.setKey(HIGH_QUALITY_AUDIO_PREF_TAG);
488             highQualityAudioPref.setVisible(false);
489             highQualityAudioPref.setOnPreferenceClickListener(clickedPref -> {
490                 boolean enable = ((TwoStatePreference) clickedPref).isChecked();
491                 a2dp.setHighQualityAudioEnabled(mCachedDevice.getDevice(), enable);
492                 return true;
493             });
494             mProfilesContainer.addPreference(highQualityAudioPref);
495         }
496     }
497 
498     @Override
onPause()499     public void onPause() {
500         for (CachedBluetoothDevice item : mAllOfCachedDevices) {
501             item.unregisterCallback(this);
502         }
503         mProfileManager.removeServiceListener(this);
504     }
505 
506     @Override
onResume()507     public void onResume() {
508         updateLeAudioConfig();
509         for (CachedBluetoothDevice item : mAllOfCachedDevices) {
510             item.registerCallback(this);
511         }
512         mProfileManager.addServiceListener(this);
513         refresh();
514     }
515 
isLeAudioOnlyDevice()516     private boolean isLeAudioOnlyDevice() {
517         if (mCachedDevice.getProfiles().stream()
518                 .noneMatch(profile -> profile instanceof LeAudioProfile)) {
519             return false;
520         }
521         return mCachedDevice.getProfiles().stream()
522                 .noneMatch(
523                         profile ->
524                                 profile instanceof HearingAidProfile
525                                         || profile instanceof A2dpProfile
526                                         || profile instanceof HeadsetProfile);
527     }
528 
updateLeAudioConfig()529     private void updateLeAudioConfig() {
530         mIsLeContactSharingEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
531                 SettingsUIDeviceConfig.BT_LE_AUDIO_CONTACT_SHARING_ENABLED, true);
532         boolean isLeAudioToggleVisible = SystemProperties.getBoolean(
533                 LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE);
534         boolean isLeEnabledByDefault =
535                 SystemProperties.getBoolean(LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY, true);
536         mIsLeAudioToggleEnabled = isLeAudioToggleVisible || isLeEnabledByDefault;
537         if (Flags.hideLeAudioToggleForLeAudioOnlyDevice() && isLeAudioOnlyDevice()) {
538             mIsLeAudioToggleEnabled = false;
539             Log.d(
540                     TAG,
541                     "Hide LeAudio toggle for LeAudio-only Device: "
542                             + mCachedDevice.getDevice().getAnonymizedAddress());
543         }
544         Log.d(TAG, "BT_LE_AUDIO_CONTACT_SHARING_ENABLED:" + mIsLeContactSharingEnabled
545                 + ", LE_AUDIO_TOGGLE_VISIBLE_PROPERTY:" + isLeAudioToggleVisible
546                 + ", LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY:" + isLeEnabledByDefault);
547     }
548 
549     @Override
onDeviceAttributesChanged()550     public void onDeviceAttributesChanged() {
551         for (CachedBluetoothDevice item : mAllOfCachedDevices) {
552             item.unregisterCallback(this);
553         }
554         mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
555         for (CachedBluetoothDevice item : mAllOfCachedDevices) {
556             item.registerCallback(this);
557         }
558 
559         super.onDeviceAttributesChanged();
560     }
561 
562     @Override
onServiceConnected()563     public void onServiceConnected() {
564         refresh();
565     }
566 
567     @Override
onServiceDisconnected()568     public void onServiceDisconnected() {
569         refresh();
570     }
571 
572     /**
573      * Refreshes the state of the switches for all profiles, possibly adding or removing switches as
574      * needed.
575      */
576     @Override
refresh()577     protected void refresh() {
578         if (Flags.enableBluetoothProfileToggleVisibilityChecker()) {
579             ThreadUtils.postOnBackgroundThread(
580                     () -> {
581                         mInvisiblePreferenceKey.set(
582                                 FeatureFactory.getFeatureFactory()
583                                         .getBluetoothFeatureProvider()
584                                         .getInvisibleProfilePreferenceKeys(
585                                                 mContext, mCachedDevice.getDevice()));
586                         ThreadUtils.postOnMainThread(this::refreshUi);
587                     });
588         } else {
589             refreshUi();
590         }
591     }
592 
refreshUi()593     private void refreshUi() {
594         for (LocalBluetoothProfile profile : getProfiles()) {
595             if (profile == null || !profile.isProfileReady()) {
596                 continue;
597             }
598             TwoStatePreference pref = mProfilesContainer.findPreference(profile.toString());
599             if (pref == null) {
600                 pref = createProfilePreference(mProfilesContainer.getContext(), profile);
601                 mProfilesContainer.addPreference(pref);
602                 maybeAddHighQualityAudioPref(profile);
603             }
604             refreshProfilePreference(pref, profile);
605         }
606         for (LocalBluetoothProfile removedProfile : mCachedDevice.getRemovedProfiles()) {
607             final TwoStatePreference pref =
608                     mProfilesContainer.findPreference(removedProfile.toString());
609             if (pref != null) {
610                 mProfilesContainer.removePreference(pref);
611             }
612         }
613 
614         Preference preference = mProfilesContainer.findPreference(KEY_BOTTOM_PREFERENCE);
615         if (preference == null) {
616             preference = new Preference(mContext);
617             preference.setLayoutResource(R.layout.preference_bluetooth_profile_category);
618             preference.setEnabled(false);
619             preference.setKey(KEY_BOTTOM_PREFERENCE);
620             preference.setOrder(ORDINAL);
621             preference.setSelectable(false);
622             mProfilesContainer.addPreference(preference);
623         }
624 
625         if (Flags.enableBluetoothProfileToggleVisibilityChecker()) {
626             Set<String> invisibleKeys = mInvisiblePreferenceKey.get();
627             if (invisibleKeys != null) {
628                 for (int i = 0; i < mProfilesContainer.getPreferenceCount(); ++i) {
629                     Preference pref = mProfilesContainer.getPreference(i);
630                     pref.setVisible(pref.isVisible() && !invisibleKeys.contains(pref.getKey()));
631                 }
632             }
633         }
634     }
635 
636     @Override
getPreferenceKey()637     public String getPreferenceKey() {
638         return KEY_PROFILES_GROUP;
639     }
640 }
641