• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.settings.SettingsEnums;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothProfile;
24 import android.content.Context;
25 import android.os.SystemProperties;
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.flags.Flags;
40 import com.android.settings.overlay.FeatureFactory;
41 import com.android.settingslib.bluetooth.A2dpProfile;
42 import com.android.settingslib.bluetooth.BluetoothUtils;
43 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
44 import com.android.settingslib.bluetooth.HeadsetProfile;
45 import com.android.settingslib.bluetooth.LeAudioProfile;
46 import com.android.settingslib.bluetooth.LocalBluetoothManager;
47 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
48 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
49 import com.android.settingslib.bluetooth.MapProfile;
50 import com.android.settingslib.bluetooth.PanProfile;
51 import com.android.settingslib.bluetooth.PbapServerProfile;
52 import com.android.settingslib.core.lifecycle.Lifecycle;
53 import com.android.settingslib.utils.ThreadUtils;
54 
55 import java.util.ArrayList;
56 import java.util.Collections;
57 import java.util.HashMap;
58 import java.util.HashSet;
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     public static final String HIGH_QUALITY_AUDIO_PREF_TAG = "A2dpProfileHighQualityAudio";
72 
73     private static final String TAG = "BtDetailsProfilesCtrl";
74 
75     private static final String KEY_PROFILES_GROUP = "bluetooth_profiles";
76     private static final String KEY_BOTTOM_PREFERENCE = "bottom_preference";
77     private static final int ORDINAL = 99;
78 
79     private static final String ENABLE_DUAL_MODE_AUDIO =
80             "persist.bluetooth.enable_dual_mode_audio";
81     private static final String LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY =
82             "ro.bluetooth.leaudio.le_audio_connection_by_default";
83     private static final boolean LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE = true;
84     private static final String LE_AUDIO_TOGGLE_VISIBLE_PROPERTY =
85             "persist.bluetooth.leaudio.toggle_visible";
86     private static final String BYPASS_LE_AUDIO_ALLOWLIST_PROPERTY =
87             "persist.bluetooth.leaudio.bypass_allow_list";
88     private static final String LE_AUDIO_TOGGLE_VISIBLE_FOR_ASHA_PROPERTY =
89             "bluetooth.leaudio.toggle_visible_for_asha";
90 
91     private Set<String> mInvisibleProfiles = Collections.emptySet();
92     private final AtomicReference<Set<String>> mAdditionalInvisibleProfiles =
93             new AtomicReference<>();
94 
95     private LocalBluetoothManager mManager;
96     private LocalBluetoothProfileManager mProfileManager;
97     private CachedBluetoothDevice mCachedDevice;
98     private Set<CachedBluetoothDevice> mCachedDeviceGroup;
99     private Map<String, List<CachedBluetoothDevice>> mProfileDeviceMap =
100             new HashMap<String, List<CachedBluetoothDevice>>();
101     private boolean mIsLeAudioToggleEnabled = false;
102     private boolean mIsLeAudioOnlyDevice = false;
103     private boolean mHasExtraSpace;
104 
105     @VisibleForTesting
106     PreferenceCategory mProfilesContainer;
107 
BluetoothDetailsProfilesController( Context context, PreferenceFragmentCompat fragment, LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle)108     public BluetoothDetailsProfilesController(
109             Context context,
110             PreferenceFragmentCompat fragment,
111             LocalBluetoothManager manager,
112             CachedBluetoothDevice device,
113             Lifecycle lifecycle) {
114         super(context, fragment, device, lifecycle);
115         mManager = manager;
116         mProfileManager = mManager.getProfileManager();
117         mCachedDevice = device;
118         mCachedDeviceGroup = Utils.findAllCachedBluetoothDevicesByGroupId(mManager, mCachedDevice);
119     }
120 
121     /** Sets the profiles to be hidden. */
setInvisibleProfiles(List<String> invisibleProfiles)122     public void setInvisibleProfiles(List<String> invisibleProfiles) {
123         if (invisibleProfiles != null) {
124             mInvisibleProfiles = Set.copyOf(invisibleProfiles);
125         }
126     }
127 
128     /** Sets whether it should show an extra padding on top of the preference. */
setHasExtraSpace(boolean hasExtraSpace)129     public void setHasExtraSpace(boolean hasExtraSpace) {
130         if (hasExtraSpace) {
131             mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
132         } else {
133             mProfilesContainer.setLayoutResource(R.layout.preference_category_bluetooth_no_padding);
134         }
135     }
136 
137     @Override
init(PreferenceScreen screen)138     protected void init(PreferenceScreen screen) {
139         mProfilesContainer = (PreferenceCategory) screen.findPreference(getPreferenceKey());
140         mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
141         // Call refresh here even though it will get called later in onResume, to avoid the
142         // list of switches appearing to "pop" into the page.
143         refresh();
144     }
145 
146     /**
147      * Creates a switch preference for the particular profile.
148      *
149      * @param context The context to use when creating the TwoStatePreference
150      * @param profile The profile for which the preference controls.
151      * @return A preference that allows the user to choose whether this profile
152      * will be connected to.
153      */
createProfilePreference(Context context, LocalBluetoothProfile profile)154     private TwoStatePreference createProfilePreference(Context context,
155             LocalBluetoothProfile profile) {
156         TwoStatePreference pref = new SwitchPreferenceCompat(context);
157         pref.setKey(profile.toString());
158         pref.setTitle(profile.getNameResource(mCachedDevice.getDevice()));
159         pref.setOnPreferenceClickListener(this);
160         pref.setOrder(profile.getOrdinal());
161 
162         boolean isLeEnabledByDefault =
163                 SystemProperties.getBoolean(LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY, true);
164 
165         if (profile instanceof LeAudioProfile && (!isLeEnabledByDefault || !isModelNameInAllowList(
166                 BluetoothUtils.getStringMetaData(mCachedDevice.getDevice(),
167                         METADATA_MODEL_NAME)))) {
168             pref.setSummary(R.string.device_details_leaudio_toggle_summary);
169         }
170         return pref;
171     }
172 
173     /**
174      * Checks if the device model name is in the LE audio allow list based on its model name.
175      *
176      * @param modelName The model name of the device to be checked.
177      * @return true if the device is in the allow list, false otherwise.
178      */
179     @VisibleForTesting
isModelNameInAllowList(String modelName)180     boolean isModelNameInAllowList(String modelName) {
181         if (modelName == null || modelName.isEmpty()) {
182             return false;
183         }
184         return BluetoothProperties.le_audio_allow_list().contains(modelName);
185     }
186 
187     /**
188      * Refreshes the state for an existing TwoStatePreference for a profile.
189      */
refreshProfilePreference(TwoStatePreference profilePref, LocalBluetoothProfile profile)190     private void refreshProfilePreference(TwoStatePreference profilePref,
191             LocalBluetoothProfile profile) {
192         BluetoothDevice device = mCachedDevice.getDevice();
193         boolean isLeAudioEnabled = isLeAudioEnabled();
194         if (profile instanceof A2dpProfile || profile instanceof HeadsetProfile
195                 || profile instanceof LeAudioProfile) {
196             List<CachedBluetoothDevice> deviceList = mProfileDeviceMap.get(
197                     profile.toString());
198             boolean isBusy = deviceList != null
199                     && deviceList.stream().anyMatch(item -> item.isBusy());
200             profilePref.setEnabled(!isBusy);
201         } else {
202             profilePref.setEnabled(!mCachedDevice.isBusy());
203         }
204 
205         if (profile instanceof LeAudioProfile) {
206             boolean showLeAudioToggle = mIsLeAudioToggleEnabled;
207             if (Flags.hideLeAudioToggleForLeAudioOnlyDevice() && mIsLeAudioOnlyDevice) {
208                 showLeAudioToggle = false;
209                 Log.d(
210                         TAG,
211                         "Hide LeAudio toggle for LeAudio-only Device: "
212                                 + mCachedDevice.getDevice().getAnonymizedAddress());
213             }
214             profilePref.setVisible(showLeAudioToggle);
215         }
216 
217         if (profile instanceof MapProfile) {
218             profilePref.setChecked(device.getMessageAccessPermission()
219                     == BluetoothDevice.ACCESS_ALLOWED);
220         } else if (profile instanceof PbapServerProfile) {
221             profilePref.setChecked(device.getPhonebookAccessPermission()
222                     == BluetoothDevice.ACCESS_ALLOWED);
223             profilePref.setSummary(profile.getSummaryResourceForDevice(mCachedDevice.getDevice()));
224         } else if (profile instanceof PanProfile) {
225             profilePref.setChecked(profile.getConnectionStatus(device) ==
226                     BluetoothProfile.STATE_CONNECTED);
227         } else {
228             profilePref.setChecked(profile.isEnabled(device));
229         }
230 
231         if (profile instanceof A2dpProfile) {
232             A2dpProfile a2dp = (A2dpProfile) profile;
233             TwoStatePreference highQualityPref =
234                     mProfilesContainer.findPreference(HIGH_QUALITY_AUDIO_PREF_TAG);
235             if (highQualityPref != null) {
236                 if (a2dp.isEnabled(device) && a2dp.supportsHighQualityAudio(device)) {
237                     highQualityPref.setVisible(true);
238                     highQualityPref.setTitle(a2dp.getHighQualityAudioOptionLabel(device));
239                     highQualityPref.setChecked(a2dp.isHighQualityAudioEnabled(device));
240                     highQualityPref.setEnabled(!mCachedDevice.isBusy());
241                 } else {
242                     highQualityPref.setVisible(false);
243                 }
244             }
245         }
246     }
247 
isLeAudioEnabled()248     private boolean isLeAudioEnabled(){
249         LocalBluetoothProfile leAudio = mProfileManager.getLeAudioProfile();
250         if (leAudio != null) {
251             List<CachedBluetoothDevice> leAudioDeviceList = mProfileDeviceMap.get(
252                     leAudio.toString());
253             if (leAudioDeviceList != null
254                     && leAudioDeviceList.stream()
255                     .anyMatch(item -> leAudio.isEnabled(item.getDevice()))) {
256                 return true;
257             }
258         }
259         return false;
260     }
261 
262     /**
263      * Helper method to enable a profile for a device.
264      */
enableProfile(LocalBluetoothProfile profile)265     private void enableProfile(LocalBluetoothProfile profile) {
266         final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
267         if (profile instanceof PbapServerProfile) {
268             bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
269             // We don't need to do the additional steps below for this profile.
270             return;
271         }
272         if (profile instanceof MapProfile) {
273             bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
274         }
275 
276         if (profile instanceof LeAudioProfile) {
277             enableLeAudioProfile(profile);
278             return;
279         }
280 
281         profile.setEnabled(bluetoothDevice, true);
282     }
283 
284     /**
285      * Helper method to disable a profile for a device
286      */
disableProfile(LocalBluetoothProfile profile)287     private void disableProfile(LocalBluetoothProfile profile) {
288         if (profile instanceof LeAudioProfile) {
289             disableLeAudioProfile(profile);
290             return;
291         }
292 
293         final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
294         profile.setEnabled(bluetoothDevice, false);
295 
296         if (profile instanceof MapProfile) {
297             bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
298         } else if (profile instanceof PbapServerProfile) {
299             bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
300         }
301     }
302 
303     /**
304      * When the pref for a bluetooth profile is clicked on, we want to toggle the enabled/disabled
305      * state for that profile.
306      */
307     @Override
onPreferenceClick(Preference preference)308     public boolean onPreferenceClick(Preference preference) {
309         LocalBluetoothProfile profile = mProfileManager.getProfileByName(preference.getKey());
310         if (profile == null) {
311             // It might be the PbapServerProfile, which is not stored by name.
312             PbapServerProfile psp = mManager.getProfileManager().getPbapProfile();
313             if (TextUtils.equals(preference.getKey(), psp.toString())) {
314                 profile = psp;
315             } else {
316                 return false;
317             }
318         }
319         TwoStatePreference profilePref = (TwoStatePreference) preference;
320         if (profilePref.isChecked()) {
321             enableProfile(profile);
322         } else {
323             disableProfile(profile);
324         }
325         refreshProfilePreference(profilePref, profile);
326         return true;
327     }
328 
329     /**
330      * Helper to get the list of connectable and special profiles.
331      */
getProfiles()332     private List<LocalBluetoothProfile> getProfiles() {
333         List<LocalBluetoothProfile> result = new ArrayList<>();
334         mProfileDeviceMap.clear();
335         if (mCachedDeviceGroup == null || mCachedDeviceGroup.isEmpty()) {
336             return result;
337         }
338         for (CachedBluetoothDevice cachedItem : mCachedDeviceGroup) {
339             List<LocalBluetoothProfile> tmpResult = cachedItem.getUiAccessibleProfiles();
340             for (LocalBluetoothProfile profile : tmpResult) {
341                 if (mProfileDeviceMap.containsKey(profile.toString())) {
342                     mProfileDeviceMap.get(profile.toString()).add(cachedItem);
343                 } else {
344                     List<CachedBluetoothDevice> tmpCachedDeviceList = new ArrayList<>();
345                     tmpCachedDeviceList.add(cachedItem);
346                     mProfileDeviceMap.put(profile.toString(), tmpCachedDeviceList);
347                     result.add(profile);
348                 }
349             }
350         }
351 
352         final BluetoothDevice device = mCachedDevice.getDevice();
353         final int pbapPermission = device.getPhonebookAccessPermission();
354         // Only provide PBAP cabability if the client device has requested PBAP.
355         if (pbapPermission != BluetoothDevice.ACCESS_UNKNOWN) {
356             final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile();
357             if (psp != null) {
358                 result.add(psp);
359             }
360         }
361 
362         final MapProfile mapProfile = mManager.getProfileManager().getMapProfile();
363         final int mapPermission = device.getMessageAccessPermission();
364         if (mapPermission != BluetoothDevice.ACCESS_UNKNOWN && mapProfile != null) {
365             result.add(mapProfile);
366         }
367 
368         // Removes phone calls & media audio toggles for dual mode devices
369         boolean leAudioSupported = result.contains(
370                 mManager.getProfileManager().getLeAudioProfile());
371         boolean classicAudioSupported = result.contains(
372                 mManager.getProfileManager().getA2dpProfile()) || result.contains(
373                 mManager.getProfileManager().getHeadsetProfile());
374         if (leAudioSupported && classicAudioSupported) {
375             result.remove(mManager.getProfileManager().getA2dpProfile());
376             result.remove(mManager.getProfileManager().getHeadsetProfile());
377         }
378         boolean hearingAidSupported = result.contains(
379                 mManager.getProfileManager().getHearingAidProfile());
380         // Remove hearing aids toggle anyway since showing the toggle will confuse users
381         if (hearingAidSupported) {
382             result.remove(mManager.getProfileManager().getHearingAidProfile());
383             if (leAudioSupported
384                     && !SystemProperties.getBoolean(BYPASS_LE_AUDIO_ALLOWLIST_PROPERTY, false)
385                     && !SystemProperties.getBoolean(
386                             LE_AUDIO_TOGGLE_VISIBLE_FOR_ASHA_PROPERTY, true)) {
387                 result.remove(mManager.getProfileManager().getLeAudioProfile());
388             }
389         }
390         if (leAudioSupported && !classicAudioSupported && !hearingAidSupported) {
391             mIsLeAudioOnlyDevice = true;
392         }
393         Log.d(TAG, "getProfiles:Map:" + mProfileDeviceMap);
394         return result;
395     }
396 
isCurrentDeviceInOrByPassAllowList()397     private boolean isCurrentDeviceInOrByPassAllowList() {
398         if (!SystemProperties.getBoolean(LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY, true)) {
399             return false;
400         }
401         return SystemProperties.getBoolean(BYPASS_LE_AUDIO_ALLOWLIST_PROPERTY, false)
402                 || isModelNameInAllowList(
403                 BluetoothUtils.getStringMetaData(
404                         mCachedDevice.getDevice(), METADATA_MODEL_NAME));
405     }
406 
407     /**
408      * Disable the Le Audio profile for each of the Le Audio devices.
409      *
410      * @param profile the LeAudio profile
411      */
disableLeAudioProfile(LocalBluetoothProfile profile)412     private void disableLeAudioProfile(LocalBluetoothProfile profile) {
413         if (profile == null || mProfileDeviceMap.get(profile.toString()) == null) {
414             Log.e(TAG, "There is no the LE profile or no device in mProfileDeviceMap. Do nothing.");
415             return;
416         }
417 
418         mMetricsFeatureProvider.action(
419                 mContext,
420                 SettingsEnums.ACTION_BLUETOOTH_PROFILE_LE_AUDIO_OFF,
421                 isCurrentDeviceInOrByPassAllowList());
422         Utils.setLeAudioEnabled(mManager, List.copyOf(mCachedDeviceGroup), false);
423     }
424 
425     /**
426      * Enable the Le Audio profile for each of the Le Audio devices.
427      *
428      * @param profile the LeAudio profile
429      */
enableLeAudioProfile(LocalBluetoothProfile profile)430     private void enableLeAudioProfile(LocalBluetoothProfile profile) {
431         if (profile == null || mProfileDeviceMap.get(profile.toString()) == null) {
432             Log.e(TAG, "There is no the LE profile or no device in mProfileDeviceMap. Do nothing.");
433             return;
434         }
435 
436         mMetricsFeatureProvider.action(
437                 mContext,
438                 SettingsEnums.ACTION_BLUETOOTH_PROFILE_LE_AUDIO_ON,
439                 isCurrentDeviceInOrByPassAllowList());
440         Utils.setLeAudioEnabled(mManager, List.copyOf(mCachedDeviceGroup), true);
441     }
442 
443     /**
444      * This is a helper method to be called after adding a Preference for a profile. If that
445      * profile happened to be A2dp and the device supports high quality audio, it will add a
446      * separate preference for controlling whether to actually use high quality audio.
447      *
448      * @param profile the profile just added
449      */
maybeAddHighQualityAudioPref(LocalBluetoothProfile profile)450     private void maybeAddHighQualityAudioPref(LocalBluetoothProfile profile) {
451         if (!(profile instanceof A2dpProfile)) {
452             return;
453         }
454         BluetoothDevice device = mCachedDevice.getDevice();
455         A2dpProfile a2dp = (A2dpProfile) profile;
456         if (a2dp.isProfileReady() && a2dp.supportsHighQualityAudio(device)) {
457             TwoStatePreference highQualityAudioPref = new SwitchPreferenceCompat(
458                     mProfilesContainer.getContext());
459             highQualityAudioPref.setKey(HIGH_QUALITY_AUDIO_PREF_TAG);
460             highQualityAudioPref.setVisible(false);
461             highQualityAudioPref.setOnPreferenceClickListener(clickedPref -> {
462                 boolean enable = ((TwoStatePreference) clickedPref).isChecked();
463                 a2dp.setHighQualityAudioEnabled(mCachedDevice.getDevice(), enable);
464                 return true;
465             });
466             mProfilesContainer.addPreference(highQualityAudioPref);
467         }
468     }
469 
470     @Override
onPause()471     public void onPause() {
472         for (CachedBluetoothDevice item : mCachedDeviceGroup) {
473             item.unregisterCallback(this);
474         }
475         mProfileManager.removeServiceListener(this);
476     }
477 
478     @Override
onResume()479     public void onResume() {
480         updateLeAudioConfig();
481         for (CachedBluetoothDevice item : mCachedDeviceGroup) {
482             item.registerCallback(this);
483         }
484         mProfileManager.addServiceListener(this);
485         refresh();
486     }
487 
updateLeAudioConfig()488     private void updateLeAudioConfig() {
489         boolean isLeAudioToggleVisible = SystemProperties.getBoolean(
490                 LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE);
491         boolean isLeEnabledByDefault =
492                 SystemProperties.getBoolean(LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY, true);
493         mIsLeAudioToggleEnabled = isLeAudioToggleVisible || isLeEnabledByDefault;
494         Log.d(TAG, "LE_AUDIO_TOGGLE_VISIBLE_PROPERTY:" + isLeAudioToggleVisible
495                 + ", LE_AUDIO_CONNECTION_BY_DEFAULT_PROPERTY:" + isLeEnabledByDefault);
496     }
497 
498     @Override
onDeviceAttributesChanged()499     public void onDeviceAttributesChanged() {
500         for (CachedBluetoothDevice item : mCachedDeviceGroup) {
501             item.unregisterCallback(this);
502         }
503         mCachedDeviceGroup = Utils.findAllCachedBluetoothDevicesByGroupId(mManager, mCachedDevice);
504         for (CachedBluetoothDevice item : mCachedDeviceGroup) {
505             item.registerCallback(this);
506         }
507 
508         super.onDeviceAttributesChanged();
509     }
510 
511     @Override
onServiceConnected()512     public void onServiceConnected() {
513         refresh();
514     }
515 
516     @Override
onServiceDisconnected()517     public void onServiceDisconnected() {
518         refresh();
519     }
520 
521     /**
522      * Refreshes the state of the switches for all profiles, possibly adding or removing switches as
523      * needed.
524      */
525     @Override
refresh()526     protected void refresh() {
527         ThreadUtils.postOnBackgroundThread(
528                 () -> {
529                     mAdditionalInvisibleProfiles.set(
530                             FeatureFactory.getFeatureFactory()
531                                     .getBluetoothFeatureProvider()
532                                     .getInvisibleProfilePreferenceKeys(
533                                             mContext, mCachedDevice.getDevice()));
534                     ThreadUtils.postOnMainThread(this::refreshUi);
535                 });
536     }
537 
refreshUi()538     private void refreshUi() {
539         for (LocalBluetoothProfile profile : getProfiles()) {
540             if (profile == null || !profile.isProfileReady()) {
541                 continue;
542             }
543             TwoStatePreference pref = mProfilesContainer.findPreference(profile.toString());
544             if (pref == null) {
545                 pref = createProfilePreference(mProfilesContainer.getContext(), profile);
546                 mProfilesContainer.addPreference(pref);
547                 maybeAddHighQualityAudioPref(profile);
548             }
549             refreshProfilePreference(pref, profile);
550         }
551         for (LocalBluetoothProfile removedProfile : mCachedDevice.getRemovedProfiles()) {
552             final TwoStatePreference pref =
553                     mProfilesContainer.findPreference(removedProfile.toString());
554             if (pref != null) {
555                 mProfilesContainer.removePreference(pref);
556             }
557         }
558 
559         Preference preference = mProfilesContainer.findPreference(KEY_BOTTOM_PREFERENCE);
560         if (preference == null) {
561             preference = new Preference(mContext);
562             if (mHasExtraSpace) {
563                 preference.setLayoutResource(R.layout.preference_bluetooth_profile_category);
564             } else {
565                 preference.setLayoutResource(R.layout.preference_category_bluetooth_no_padding);
566             }
567             preference.setEnabled(false);
568             preference.setKey(KEY_BOTTOM_PREFERENCE);
569             preference.setOrder(ORDINAL);
570             preference.setSelectable(false);
571             mProfilesContainer.addPreference(preference);
572         }
573 
574         Set<String> additionalInvisibleProfiles = mAdditionalInvisibleProfiles.get();
575         HashSet<String> combinedInvisibleProfiles = new HashSet<>(mInvisibleProfiles);
576         if (additionalInvisibleProfiles != null) {
577             combinedInvisibleProfiles.addAll(additionalInvisibleProfiles);
578         }
579         Log.i(TAG, "Invisible profiles: " + combinedInvisibleProfiles);
580         for (int i = 0; i < mProfilesContainer.getPreferenceCount(); ++i) {
581             Preference pref = mProfilesContainer.getPreference(i);
582             pref.setVisible(pref.isVisible() && !combinedInvisibleProfiles.contains(pref.getKey()));
583         }
584     }
585 
586     @Override
getPreferenceKey()587     public String getPreferenceKey() {
588         return KEY_PROFILES_GROUP;
589     }
590 }
591