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