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