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 android.bluetooth.BluetoothDevice; 20 import android.bluetooth.BluetoothProfile; 21 import android.content.Context; 22 import android.os.SystemProperties; 23 import android.provider.DeviceConfig; 24 import android.text.TextUtils; 25 import android.util.Log; 26 27 import androidx.annotation.VisibleForTesting; 28 import androidx.preference.Preference; 29 import androidx.preference.PreferenceCategory; 30 import androidx.preference.PreferenceFragmentCompat; 31 import androidx.preference.PreferenceScreen; 32 import androidx.preference.SwitchPreference; 33 34 import com.android.settings.R; 35 import com.android.settings.core.SettingsUIDeviceConfig; 36 import com.android.settingslib.bluetooth.A2dpProfile; 37 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 38 import com.android.settingslib.bluetooth.HeadsetProfile; 39 import com.android.settingslib.bluetooth.LeAudioProfile; 40 import com.android.settingslib.bluetooth.LocalBluetoothManager; 41 import com.android.settingslib.bluetooth.LocalBluetoothProfile; 42 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 43 import com.android.settingslib.bluetooth.MapProfile; 44 import com.android.settingslib.bluetooth.PanProfile; 45 import com.android.settingslib.bluetooth.PbapServerProfile; 46 import com.android.settingslib.core.lifecycle.Lifecycle; 47 48 import java.util.ArrayList; 49 import java.util.HashMap; 50 import java.util.List; 51 import java.util.Map; 52 53 /** 54 * This class adds switches for toggling the individual profiles that a Bluetooth device 55 * supports, such as "Phone audio", "Media audio", "Contact sharing", etc. 56 */ 57 public class BluetoothDetailsProfilesController extends BluetoothDetailsController 58 implements Preference.OnPreferenceClickListener, 59 LocalBluetoothProfileManager.ServiceListener { 60 private static final String TAG = "BtDetailsProfilesCtrl"; 61 62 private static final String KEY_PROFILES_GROUP = "bluetooth_profiles"; 63 private static final String KEY_BOTTOM_PREFERENCE = "bottom_preference"; 64 private static final int ORDINAL = 99; 65 66 @VisibleForTesting 67 static final String HIGH_QUALITY_AUDIO_PREF_TAG = "A2dpProfileHighQualityAudio"; 68 69 private static final String ENABLE_DUAL_MODE_AUDIO = 70 "persist.bluetooth.enable_dual_mode_audio"; 71 private static final String CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT = "le_audio_enabled_by_default"; 72 private static final boolean LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE = true; 73 private static final String LE_AUDIO_TOGGLE_VISIBLE_PROPERTY = 74 "persist.bluetooth.leaudio.toggle_visible"; 75 76 private LocalBluetoothManager mManager; 77 private LocalBluetoothProfileManager mProfileManager; 78 private CachedBluetoothDevice mCachedDevice; 79 private List<CachedBluetoothDevice> mAllOfCachedDevices; 80 private Map<String, List<CachedBluetoothDevice>> mProfileDeviceMap = 81 new HashMap<String, List<CachedBluetoothDevice>>(); 82 private boolean mIsLeContactSharingEnabled = false; 83 private boolean mIsLeAudioToggleEnabled = false; 84 85 @VisibleForTesting 86 PreferenceCategory mProfilesContainer; 87 BluetoothDetailsProfilesController(Context context, PreferenceFragmentCompat fragment, LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle)88 public BluetoothDetailsProfilesController(Context context, PreferenceFragmentCompat fragment, 89 LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle) { 90 super(context, fragment, device, lifecycle); 91 mManager = manager; 92 mProfileManager = mManager.getProfileManager(); 93 mCachedDevice = device; 94 mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice); 95 lifecycle.addObserver(this); 96 } 97 98 @Override init(PreferenceScreen screen)99 protected void init(PreferenceScreen screen) { 100 mProfilesContainer = (PreferenceCategory)screen.findPreference(getPreferenceKey()); 101 mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category); 102 // Call refresh here even though it will get called later in onResume, to avoid the 103 // list of switches appearing to "pop" into the page. 104 refresh(); 105 } 106 107 /** 108 * Creates a switch preference for the particular profile. 109 * 110 * @param context The context to use when creating the SwitchPreference 111 * @param profile The profile for which the preference controls. 112 * @return A preference that allows the user to choose whether this profile 113 * will be connected to. 114 */ createProfilePreference(Context context, LocalBluetoothProfile profile)115 private SwitchPreference createProfilePreference(Context context, 116 LocalBluetoothProfile profile) { 117 SwitchPreference pref = new SwitchPreference(context); 118 pref.setKey(profile.toString()); 119 pref.setTitle(profile.getNameResource(mCachedDevice.getDevice())); 120 pref.setOnPreferenceClickListener(this); 121 pref.setOrder(profile.getOrdinal()); 122 123 if (profile instanceof LeAudioProfile) { 124 pref.setSummary(R.string.device_details_leaudio_toggle_summary); 125 } 126 return pref; 127 } 128 129 /** 130 * Refreshes the state for an existing SwitchPreference for a profile. 131 */ refreshProfilePreference(SwitchPreference profilePref, LocalBluetoothProfile profile)132 private void refreshProfilePreference(SwitchPreference profilePref, 133 LocalBluetoothProfile profile) { 134 BluetoothDevice device = mCachedDevice.getDevice(); 135 boolean isLeAudioEnabled = isLeAudioEnabled(); 136 if (profile instanceof A2dpProfile || profile instanceof HeadsetProfile 137 || profile instanceof LeAudioProfile) { 138 List<CachedBluetoothDevice> deviceList = mProfileDeviceMap.get( 139 profile.toString()); 140 boolean isBusy = deviceList != null 141 && deviceList.stream().anyMatch(item -> item.isBusy()); 142 profilePref.setEnabled(!isBusy); 143 } else if (profile instanceof PbapServerProfile 144 && isLeAudioEnabled 145 && !mIsLeContactSharingEnabled) { 146 profilePref.setEnabled(false); 147 } else { 148 profilePref.setEnabled(!mCachedDevice.isBusy()); 149 } 150 151 if (profile instanceof LeAudioProfile) { 152 profilePref.setVisible(mIsLeAudioToggleEnabled); 153 } 154 155 if (profile instanceof MapProfile) { 156 profilePref.setChecked(device.getMessageAccessPermission() 157 == BluetoothDevice.ACCESS_ALLOWED); 158 } else if (profile instanceof PbapServerProfile) { 159 profilePref.setChecked(device.getPhonebookAccessPermission() 160 == BluetoothDevice.ACCESS_ALLOWED); 161 } else if (profile instanceof PanProfile) { 162 profilePref.setChecked(profile.getConnectionStatus(device) == 163 BluetoothProfile.STATE_CONNECTED); 164 } else { 165 profilePref.setChecked(profile.isEnabled(device)); 166 } 167 168 if (profile instanceof A2dpProfile) { 169 A2dpProfile a2dp = (A2dpProfile) profile; 170 SwitchPreference highQualityPref = (SwitchPreference) mProfilesContainer.findPreference( 171 HIGH_QUALITY_AUDIO_PREF_TAG); 172 if (highQualityPref != null) { 173 if (a2dp.isEnabled(device) && a2dp.supportsHighQualityAudio(device)) { 174 highQualityPref.setVisible(true); 175 highQualityPref.setTitle(a2dp.getHighQualityAudioOptionLabel(device)); 176 highQualityPref.setChecked(a2dp.isHighQualityAudioEnabled(device)); 177 highQualityPref.setEnabled(!mCachedDevice.isBusy()); 178 } else { 179 highQualityPref.setVisible(false); 180 } 181 } 182 } 183 } 184 isLeAudioEnabled()185 private boolean isLeAudioEnabled(){ 186 LocalBluetoothProfile leAudio = mProfileManager.getLeAudioProfile(); 187 if (leAudio != null) { 188 List<CachedBluetoothDevice> leAudioDeviceList = mProfileDeviceMap.get( 189 leAudio.toString()); 190 if (leAudioDeviceList != null 191 && leAudioDeviceList.stream() 192 .anyMatch(item -> leAudio.isEnabled(item.getDevice()))) { 193 return true; 194 } 195 } 196 return false; 197 } 198 199 /** 200 * Helper method to enable a profile for a device. 201 */ enableProfile(LocalBluetoothProfile profile)202 private void enableProfile(LocalBluetoothProfile profile) { 203 final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); 204 if (profile instanceof PbapServerProfile) { 205 bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 206 // We don't need to do the additional steps below for this profile. 207 return; 208 } 209 if (profile instanceof MapProfile) { 210 bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 211 } 212 213 if (profile instanceof LeAudioProfile) { 214 enableLeAudioProfile(profile); 215 return; 216 } 217 218 profile.setEnabled(bluetoothDevice, true); 219 } 220 221 /** 222 * Helper method to disable a profile for a device 223 */ disableProfile(LocalBluetoothProfile profile)224 private void disableProfile(LocalBluetoothProfile profile) { 225 if (profile instanceof LeAudioProfile) { 226 disableLeAudioProfile(profile); 227 return; 228 } 229 230 final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); 231 profile.setEnabled(bluetoothDevice, false); 232 233 if (profile instanceof MapProfile) { 234 bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 235 } else if (profile instanceof PbapServerProfile) { 236 bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 237 } 238 } 239 240 /** 241 * When the pref for a bluetooth profile is clicked on, we want to toggle the enabled/disabled 242 * state for that profile. 243 */ 244 @Override onPreferenceClick(Preference preference)245 public boolean onPreferenceClick(Preference preference) { 246 LocalBluetoothProfile profile = mProfileManager.getProfileByName(preference.getKey()); 247 if (profile == null) { 248 // It might be the PbapServerProfile, which is not stored by name. 249 PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); 250 if (TextUtils.equals(preference.getKey(), psp.toString())) { 251 profile = psp; 252 } else { 253 return false; 254 } 255 } 256 SwitchPreference profilePref = (SwitchPreference) preference; 257 if (profilePref.isChecked()) { 258 enableProfile(profile); 259 } else { 260 disableProfile(profile); 261 } 262 refreshProfilePreference(profilePref, profile); 263 return true; 264 } 265 266 /** 267 * Helper to get the list of connectable and special profiles. 268 */ getProfiles()269 private List<LocalBluetoothProfile> getProfiles() { 270 List<LocalBluetoothProfile> result = new ArrayList<LocalBluetoothProfile>(); 271 mProfileDeviceMap.clear(); 272 if (mAllOfCachedDevices == null || mAllOfCachedDevices.isEmpty()) { 273 return result; 274 } 275 for (CachedBluetoothDevice cachedItem : mAllOfCachedDevices) { 276 List<LocalBluetoothProfile> tmpResult = cachedItem.getConnectableProfiles(); 277 for (LocalBluetoothProfile profile : tmpResult) { 278 if (mProfileDeviceMap.containsKey(profile.toString())) { 279 mProfileDeviceMap.get(profile.toString()).add(cachedItem); 280 } else { 281 List<CachedBluetoothDevice> tmpCachedDeviceList = 282 new ArrayList<CachedBluetoothDevice>(); 283 tmpCachedDeviceList.add(cachedItem); 284 mProfileDeviceMap.put(profile.toString(), tmpCachedDeviceList); 285 result.add(profile); 286 } 287 } 288 } 289 290 final BluetoothDevice device = mCachedDevice.getDevice(); 291 final int pbapPermission = device.getPhonebookAccessPermission(); 292 // Only provide PBAP cabability if the client device has requested PBAP. 293 if (pbapPermission != BluetoothDevice.ACCESS_UNKNOWN) { 294 final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); 295 result.add(psp); 296 } 297 298 final MapProfile mapProfile = mManager.getProfileManager().getMapProfile(); 299 final int mapPermission = device.getMessageAccessPermission(); 300 if (mapPermission != BluetoothDevice.ACCESS_UNKNOWN) { 301 result.add(mapProfile); 302 } 303 304 // Removes phone calls & media audio toggles for dual mode devices 305 boolean leAudioSupported = result.contains( 306 mManager.getProfileManager().getLeAudioProfile()); 307 boolean classicAudioSupported = result.contains( 308 mManager.getProfileManager().getA2dpProfile()) || result.contains( 309 mManager.getProfileManager().getHeadsetProfile()); 310 if (leAudioSupported && classicAudioSupported) { 311 result.remove(mManager.getProfileManager().getA2dpProfile()); 312 result.remove(mManager.getProfileManager().getHeadsetProfile()); 313 } 314 Log.d(TAG, "getProfiles:Map:" + mProfileDeviceMap); 315 return result; 316 } 317 318 /** 319 * Disable the Le Audio profile for each of the Le Audio devices. 320 * 321 * @param profile the LeAudio profile 322 */ disableLeAudioProfile(LocalBluetoothProfile profile)323 private void disableLeAudioProfile(LocalBluetoothProfile profile) { 324 if (profile == null || mProfileDeviceMap.get(profile.toString()) == null) { 325 Log.e(TAG, "There is no the LE profile or no device in mProfileDeviceMap. Do nothing."); 326 return; 327 } 328 329 LocalBluetoothProfile asha = mProfileManager.getHearingAidProfile(); 330 331 for (CachedBluetoothDevice leAudioDevice : mProfileDeviceMap.get(profile.toString())) { 332 Log.d(TAG, 333 "device:" + leAudioDevice.getDevice().getAnonymizedAddress() 334 + "disable LE profile"); 335 profile.setEnabled(leAudioDevice.getDevice(), false); 336 if (asha != null) { 337 asha.setEnabled(leAudioDevice.getDevice(), true); 338 } 339 } 340 341 if (!SystemProperties.getBoolean(ENABLE_DUAL_MODE_AUDIO, false)) { 342 Log.i(TAG, "Enabling classic audio profiles because dual mode is disabled"); 343 enableProfileAfterUserDisablesLeAudio(mProfileManager.getA2dpProfile()); 344 enableProfileAfterUserDisablesLeAudio(mProfileManager.getHeadsetProfile()); 345 } 346 } 347 348 /** 349 * Enable the Le Audio profile for each of the Le Audio devices. 350 * 351 * @param profile the LeAudio profile 352 */ enableLeAudioProfile(LocalBluetoothProfile profile)353 private void enableLeAudioProfile(LocalBluetoothProfile profile) { 354 if (profile == null || mProfileDeviceMap.get(profile.toString()) == null) { 355 Log.e(TAG, "There is no the LE profile or no device in mProfileDeviceMap. Do nothing."); 356 return; 357 } 358 359 if (!SystemProperties.getBoolean(ENABLE_DUAL_MODE_AUDIO, false)) { 360 Log.i(TAG, "Disabling classic audio profiles because dual mode is disabled"); 361 disableProfileBeforeUserEnablesLeAudio(mProfileManager.getA2dpProfile()); 362 disableProfileBeforeUserEnablesLeAudio(mProfileManager.getHeadsetProfile()); 363 } 364 LocalBluetoothProfile asha = mProfileManager.getHearingAidProfile(); 365 366 for (CachedBluetoothDevice leAudioDevice : mProfileDeviceMap.get(profile.toString())) { 367 Log.d(TAG, 368 "device:" + leAudioDevice.getDevice().getAnonymizedAddress() 369 + "enable LE profile"); 370 profile.setEnabled(leAudioDevice.getDevice(), true); 371 if (asha != null) { 372 asha.setEnabled(leAudioDevice.getDevice(), false); 373 } 374 } 375 } 376 disableProfileBeforeUserEnablesLeAudio(LocalBluetoothProfile profile)377 private void disableProfileBeforeUserEnablesLeAudio(LocalBluetoothProfile profile) { 378 if (profile != null && mProfileDeviceMap.get(profile.toString()) != null) { 379 Log.d(TAG, "Disable " + profile.toString() + " before user enables LE"); 380 for (CachedBluetoothDevice profileDevice : mProfileDeviceMap.get(profile.toString())) { 381 if (profile.isEnabled(profileDevice.getDevice())) { 382 Log.d(TAG, "The " + profileDevice.getDevice().getAnonymizedAddress() + ":" 383 + profile.toString() + " set disable"); 384 profile.setEnabled(profileDevice.getDevice(), false); 385 } else { 386 Log.d(TAG, "The " + profileDevice.getDevice().getAnonymizedAddress() + ":" 387 + profile.toString() + " profile is disabled. Do nothing."); 388 } 389 } 390 } else { 391 if (profile == null) { 392 Log.w(TAG, "profile is null"); 393 } else { 394 Log.w(TAG, profile.toString() + " is not in " + mProfileDeviceMap); 395 } 396 } 397 } 398 enableProfileAfterUserDisablesLeAudio(LocalBluetoothProfile profile)399 private void enableProfileAfterUserDisablesLeAudio(LocalBluetoothProfile profile) { 400 if (profile != null && mProfileDeviceMap.get(profile.toString()) != null) { 401 Log.d(TAG, "enable " + profile.toString() + "after user disables LE"); 402 for (CachedBluetoothDevice profileDevice : mProfileDeviceMap.get(profile.toString())) { 403 if (!profile.isEnabled(profileDevice.getDevice())) { 404 Log.d(TAG, "The " + profileDevice.getDevice().getAnonymizedAddress() + ":" 405 + profile.toString() + " set enable"); 406 profile.setEnabled(profileDevice.getDevice(), true); 407 } else { 408 Log.d(TAG, "The " + profileDevice.getDevice().getAnonymizedAddress() + ":" 409 + profile.toString() + " profile is enabled. Do nothing."); 410 } 411 } 412 } else { 413 if (profile == null) { 414 Log.w(TAG, "profile is null"); 415 } else { 416 Log.w(TAG, profile.toString() + " is not in " + mProfileDeviceMap); 417 } 418 } 419 } 420 421 /** 422 * This is a helper method to be called after adding a Preference for a profile. If that 423 * profile happened to be A2dp and the device supports high quality audio, it will add a 424 * separate preference for controlling whether to actually use high quality audio. 425 * 426 * @param profile the profile just added 427 */ maybeAddHighQualityAudioPref(LocalBluetoothProfile profile)428 private void maybeAddHighQualityAudioPref(LocalBluetoothProfile profile) { 429 if (!(profile instanceof A2dpProfile)) { 430 return; 431 } 432 BluetoothDevice device = mCachedDevice.getDevice(); 433 A2dpProfile a2dp = (A2dpProfile) profile; 434 if (a2dp.isProfileReady() && a2dp.supportsHighQualityAudio(device)) { 435 SwitchPreference highQualityAudioPref = new SwitchPreference( 436 mProfilesContainer.getContext()); 437 highQualityAudioPref.setKey(HIGH_QUALITY_AUDIO_PREF_TAG); 438 highQualityAudioPref.setVisible(false); 439 highQualityAudioPref.setOnPreferenceClickListener(clickedPref -> { 440 boolean enable = ((SwitchPreference) clickedPref).isChecked(); 441 a2dp.setHighQualityAudioEnabled(mCachedDevice.getDevice(), enable); 442 return true; 443 }); 444 mProfilesContainer.addPreference(highQualityAudioPref); 445 } 446 } 447 448 @Override onPause()449 public void onPause() { 450 for (CachedBluetoothDevice item : mAllOfCachedDevices) { 451 item.unregisterCallback(this); 452 } 453 mProfileManager.removeServiceListener(this); 454 } 455 456 @Override onResume()457 public void onResume() { 458 updateLeAudioConfig(); 459 for (CachedBluetoothDevice item : mAllOfCachedDevices) { 460 item.registerCallback(this); 461 } 462 mProfileManager.addServiceListener(this); 463 refresh(); 464 } 465 updateLeAudioConfig()466 private void updateLeAudioConfig() { 467 mIsLeContactSharingEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, 468 SettingsUIDeviceConfig.BT_LE_AUDIO_CONTACT_SHARING_ENABLED, true); 469 boolean isLeAudioToggleVisible = SystemProperties.getBoolean( 470 LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE); 471 boolean isLeEnabledByDefault = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, 472 CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT, false); 473 mIsLeAudioToggleEnabled = isLeAudioToggleVisible || isLeEnabledByDefault; 474 Log.d(TAG, "BT_LE_AUDIO_CONTACT_SHARING_ENABLED:" + mIsLeContactSharingEnabled 475 + ", LE_AUDIO_TOGGLE_VISIBLE_PROPERTY:" + isLeAudioToggleVisible 476 + ", CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT:" + isLeEnabledByDefault); 477 } 478 479 @Override onDeviceAttributesChanged()480 public void onDeviceAttributesChanged() { 481 for (CachedBluetoothDevice item : mAllOfCachedDevices) { 482 item.unregisterCallback(this); 483 } 484 mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice); 485 for (CachedBluetoothDevice item : mAllOfCachedDevices) { 486 item.registerCallback(this); 487 } 488 489 super.onDeviceAttributesChanged(); 490 } 491 492 @Override onServiceConnected()493 public void onServiceConnected() { 494 refresh(); 495 } 496 497 @Override onServiceDisconnected()498 public void onServiceDisconnected() { 499 refresh(); 500 } 501 502 /** 503 * Refreshes the state of the switches for all profiles, possibly adding or removing switches as 504 * needed. 505 */ 506 @Override refresh()507 protected void refresh() { 508 for (LocalBluetoothProfile profile : getProfiles()) { 509 if (profile == null || !profile.isProfileReady()) { 510 continue; 511 } 512 SwitchPreference pref = mProfilesContainer.findPreference( 513 profile.toString()); 514 if (pref == null) { 515 pref = createProfilePreference(mProfilesContainer.getContext(), profile); 516 mProfilesContainer.addPreference(pref); 517 maybeAddHighQualityAudioPref(profile); 518 } 519 refreshProfilePreference(pref, profile); 520 } 521 for (LocalBluetoothProfile removedProfile : mCachedDevice.getRemovedProfiles()) { 522 final SwitchPreference pref = mProfilesContainer.findPreference( 523 removedProfile.toString()); 524 if (pref != null) { 525 mProfilesContainer.removePreference(pref); 526 } 527 } 528 529 Preference preference = mProfilesContainer.findPreference(KEY_BOTTOM_PREFERENCE); 530 if (preference == null) { 531 preference = new Preference(mContext); 532 preference.setLayoutResource(R.layout.preference_bluetooth_profile_category); 533 preference.setEnabled(false); 534 preference.setKey(KEY_BOTTOM_PREFERENCE); 535 preference.setOrder(ORDINAL); 536 preference.setSelectable(false); 537 mProfilesContainer.addPreference(preference); 538 } 539 } 540 541 @Override getPreferenceKey()542 public String getPreferenceKey() { 543 return KEY_PROFILES_GROUP; 544 } 545 } 546