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.BluetoothCsipSetCoordinator; 20 import android.bluetooth.BluetoothDevice; 21 import android.bluetooth.BluetoothProfile; 22 import android.content.Context; 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 LocalBluetoothManager mManager; 70 private LocalBluetoothProfileManager mProfileManager; 71 private CachedBluetoothDevice mCachedDevice; 72 private List<CachedBluetoothDevice> mAllOfCachedDevices; 73 private Map<String, List<CachedBluetoothDevice>> mProfileDeviceMap = 74 new HashMap<String, List<CachedBluetoothDevice>>(); 75 private boolean mIsLeContactSharingEnabled = false; 76 77 @VisibleForTesting 78 PreferenceCategory mProfilesContainer; 79 BluetoothDetailsProfilesController(Context context, PreferenceFragmentCompat fragment, LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle)80 public BluetoothDetailsProfilesController(Context context, PreferenceFragmentCompat fragment, 81 LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle) { 82 super(context, fragment, device, lifecycle); 83 mManager = manager; 84 mProfileManager = mManager.getProfileManager(); 85 mCachedDevice = device; 86 mAllOfCachedDevices = getAllOfCachedBluetoothDevices(); 87 lifecycle.addObserver(this); 88 } 89 90 @Override init(PreferenceScreen screen)91 protected void init(PreferenceScreen screen) { 92 mProfilesContainer = (PreferenceCategory)screen.findPreference(getPreferenceKey()); 93 mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category); 94 mIsLeContactSharingEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, 95 SettingsUIDeviceConfig.BT_LE_AUDIO_CONTACT_SHARING_ENABLED, true); 96 // Call refresh here even though it will get called later in onResume, to avoid the 97 // list of switches appearing to "pop" into the page. 98 refresh(); 99 } 100 101 /** 102 * Creates a switch preference for the particular profile. 103 * 104 * @param context The context to use when creating the SwitchPreference 105 * @param profile The profile for which the preference controls. 106 * @return A preference that allows the user to choose whether this profile 107 * will be connected to. 108 */ createProfilePreference(Context context, LocalBluetoothProfile profile)109 private SwitchPreference createProfilePreference(Context context, 110 LocalBluetoothProfile profile) { 111 SwitchPreference pref = new SwitchPreference(context); 112 pref.setKey(profile.toString()); 113 pref.setTitle(profile.getNameResource(mCachedDevice.getDevice())); 114 pref.setOnPreferenceClickListener(this); 115 pref.setOrder(profile.getOrdinal()); 116 return pref; 117 } 118 119 /** 120 * Refreshes the state for an existing SwitchPreference for a profile. 121 * If the LeAudio profile is enabled on the LeAudio devices, then the SwitchPreferences of 122 * A2dp profile and Headset profile are graied out. 123 */ refreshProfilePreference(SwitchPreference profilePref, LocalBluetoothProfile profile)124 private void refreshProfilePreference(SwitchPreference profilePref, 125 LocalBluetoothProfile profile) { 126 BluetoothDevice device = mCachedDevice.getDevice(); 127 boolean isLeAudioEnabled = isLeAudioEnabled(); 128 if (profile instanceof A2dpProfile 129 || profile instanceof HeadsetProfile) { 130 if (isLeAudioEnabled) { 131 // If the LeAudio profile is enabled on the LeAudio devices, then the 132 // SwitchPreferences of A2dp profile and Headset profile are grayed out. 133 Log.d(TAG, "LE is enabled, gray out " + profile.toString()); 134 profilePref.setEnabled(false); 135 } else { 136 List<CachedBluetoothDevice> deviceList = mProfileDeviceMap.get( 137 profile.toString()); 138 boolean isBusy = deviceList != null 139 && deviceList.stream().anyMatch(item -> item.isBusy()); 140 profilePref.setEnabled(!isBusy); 141 } 142 } else if (profile instanceof LeAudioProfile) { 143 List<CachedBluetoothDevice> leAudioDeviceList = mProfileDeviceMap.get( 144 profile.toString()); 145 boolean isBusy = leAudioDeviceList != null 146 && leAudioDeviceList.stream().anyMatch(item -> item.isBusy()); 147 if (isLeAudioEnabled && !isBusy) { 148 LocalBluetoothProfile a2dp = mProfileManager.getA2dpProfile(); 149 LocalBluetoothProfile headset = mProfileManager.getHeadsetProfile(); 150 // If the LeAudio profile is enabled on the LeAudio devices, then the 151 // SwitchPreferences of A2dp profile and Headset profile are graied out. 152 grayOutPreferenceWhenLeAudioIsEnabled(a2dp); 153 grayOutPreferenceWhenLeAudioIsEnabled(headset); 154 } 155 profilePref.setEnabled(!isBusy); 156 } else if (profile instanceof PbapServerProfile 157 && isLeAudioEnabled 158 && !mIsLeContactSharingEnabled) { 159 profilePref.setEnabled(false); 160 } else { 161 profilePref.setEnabled(!mCachedDevice.isBusy()); 162 } 163 164 if (profile instanceof MapProfile) { 165 profilePref.setChecked(device.getMessageAccessPermission() 166 == BluetoothDevice.ACCESS_ALLOWED); 167 } else if (profile instanceof PbapServerProfile) { 168 profilePref.setChecked(device.getPhonebookAccessPermission() 169 == BluetoothDevice.ACCESS_ALLOWED); 170 } else if (profile instanceof PanProfile) { 171 profilePref.setChecked(profile.getConnectionStatus(device) == 172 BluetoothProfile.STATE_CONNECTED); 173 } else { 174 profilePref.setChecked(profile.isEnabled(device)); 175 } 176 177 if (profile instanceof A2dpProfile) { 178 A2dpProfile a2dp = (A2dpProfile) profile; 179 SwitchPreference highQualityPref = (SwitchPreference) mProfilesContainer.findPreference( 180 HIGH_QUALITY_AUDIO_PREF_TAG); 181 if (highQualityPref != null) { 182 if (a2dp.isEnabled(device) && a2dp.supportsHighQualityAudio(device)) { 183 highQualityPref.setVisible(true); 184 highQualityPref.setTitle(a2dp.getHighQualityAudioOptionLabel(device)); 185 highQualityPref.setChecked(a2dp.isHighQualityAudioEnabled(device)); 186 highQualityPref.setEnabled(!mCachedDevice.isBusy() && !isLeAudioEnabled); 187 } else { 188 highQualityPref.setVisible(false); 189 } 190 } 191 } 192 } 193 isLeAudioEnabled()194 private boolean isLeAudioEnabled(){ 195 LocalBluetoothProfile leAudio = mProfileManager.getLeAudioProfile(); 196 if (leAudio != null) { 197 List<CachedBluetoothDevice> leAudioDeviceList = mProfileDeviceMap.get( 198 leAudio.toString()); 199 if (leAudioDeviceList != null 200 && leAudioDeviceList.stream() 201 .anyMatch(item -> leAudio.isEnabled(item.getDevice()))) { 202 return true; 203 } 204 } 205 return false; 206 } 207 grayOutPreferenceWhenLeAudioIsEnabled(LocalBluetoothProfile profile)208 private void grayOutPreferenceWhenLeAudioIsEnabled(LocalBluetoothProfile profile) { 209 if (profile != null) { 210 SwitchPreference pref = mProfilesContainer.findPreference(profile.toString()); 211 if (pref != null) { 212 Log.d(TAG, "LE is enabled, gray out " + profile.toString()); 213 pref.setEnabled(false); 214 } 215 } 216 } 217 218 /** 219 * Helper method to enable a profile for a device. 220 */ enableProfile(LocalBluetoothProfile profile)221 private void enableProfile(LocalBluetoothProfile profile) { 222 final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); 223 if (profile instanceof PbapServerProfile) { 224 bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 225 // We don't need to do the additional steps below for this profile. 226 return; 227 } 228 if (profile instanceof MapProfile) { 229 bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 230 } 231 232 if (profile instanceof LeAudioProfile) { 233 enableLeAudioProfile(profile); 234 return; 235 } 236 237 profile.setEnabled(bluetoothDevice, true); 238 } 239 240 /** 241 * Helper method to disable a profile for a device 242 */ disableProfile(LocalBluetoothProfile profile)243 private void disableProfile(LocalBluetoothProfile profile) { 244 if (profile instanceof LeAudioProfile) { 245 disableLeAudioProfile(profile); 246 return; 247 } 248 249 final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); 250 profile.setEnabled(bluetoothDevice, false); 251 252 if (profile instanceof MapProfile) { 253 bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 254 } else if (profile instanceof PbapServerProfile) { 255 bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 256 } 257 } 258 259 /** 260 * When the pref for a bluetooth profile is clicked on, we want to toggle the enabled/disabled 261 * state for that profile. 262 */ 263 @Override onPreferenceClick(Preference preference)264 public boolean onPreferenceClick(Preference preference) { 265 LocalBluetoothProfile profile = mProfileManager.getProfileByName(preference.getKey()); 266 if (profile == null) { 267 // It might be the PbapServerProfile, which is not stored by name. 268 PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); 269 if (TextUtils.equals(preference.getKey(), psp.toString())) { 270 profile = psp; 271 } else { 272 return false; 273 } 274 } 275 SwitchPreference profilePref = (SwitchPreference) preference; 276 if (profilePref.isChecked()) { 277 enableProfile(profile); 278 } else { 279 disableProfile(profile); 280 } 281 refreshProfilePreference(profilePref, profile); 282 return true; 283 } 284 285 /** 286 * Helper to get the list of connectable and special profiles. 287 */ getProfiles()288 private List<LocalBluetoothProfile> getProfiles() { 289 List<LocalBluetoothProfile> result = new ArrayList<LocalBluetoothProfile>(); 290 mProfileDeviceMap.clear(); 291 if (mAllOfCachedDevices == null || mAllOfCachedDevices.isEmpty()) { 292 return result; 293 } 294 for (CachedBluetoothDevice cachedItem : mAllOfCachedDevices) { 295 List<LocalBluetoothProfile> tmpResult = cachedItem.getConnectableProfiles(); 296 for (LocalBluetoothProfile profile : tmpResult) { 297 if (mProfileDeviceMap.containsKey(profile.toString())) { 298 mProfileDeviceMap.get(profile.toString()).add(cachedItem); 299 Log.d(TAG, "getProfiles: " + profile.toString() + " add device " 300 + cachedItem.getDevice().getAnonymizedAddress()); 301 } else { 302 List<CachedBluetoothDevice> tmpCachedDeviceList = 303 new ArrayList<CachedBluetoothDevice>(); 304 tmpCachedDeviceList.add(cachedItem); 305 mProfileDeviceMap.put(profile.toString(), tmpCachedDeviceList); 306 result.add(profile); 307 } 308 } 309 } 310 311 final BluetoothDevice device = mCachedDevice.getDevice(); 312 final int pbapPermission = device.getPhonebookAccessPermission(); 313 // Only provide PBAP cabability if the client device has requested PBAP. 314 if (pbapPermission != BluetoothDevice.ACCESS_UNKNOWN) { 315 final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); 316 result.add(psp); 317 } 318 319 final MapProfile mapProfile = mManager.getProfileManager().getMapProfile(); 320 final int mapPermission = device.getMessageAccessPermission(); 321 if (mapPermission != BluetoothDevice.ACCESS_UNKNOWN) { 322 result.add(mapProfile); 323 } 324 Log.d(TAG, "getProfiles:result:" + result); 325 return result; 326 } 327 getAllOfCachedBluetoothDevices()328 private List<CachedBluetoothDevice> getAllOfCachedBluetoothDevices() { 329 List<CachedBluetoothDevice> cachedBluetoothDevices = new ArrayList<>(); 330 if (mCachedDevice == null) { 331 return cachedBluetoothDevices; 332 } 333 cachedBluetoothDevices.add(mCachedDevice); 334 if (mCachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 335 for (CachedBluetoothDevice member : mCachedDevice.getMemberDevice()) { 336 cachedBluetoothDevices.add(member); 337 } 338 } 339 return cachedBluetoothDevices; 340 } 341 342 /** 343 * When user disable the Le Audio profile, the system needs to do two things. 344 * 1) Disable the Le Audio profile, VCP and CSIP for each of the Le Audio devices. 345 * 2) Enable the A2dp profile and Headset profile for the associated device. The system 346 * can't enable the A2dp profile and Headset profile if the Le Audio profile is enabled. 347 * 348 * @param profile the LeAudio profile 349 */ disableLeAudioProfile(LocalBluetoothProfile profile)350 private void disableLeAudioProfile(LocalBluetoothProfile profile) { 351 if (profile == null || mProfileDeviceMap.get(profile.toString()) == null) { 352 Log.e(TAG, "There is no the LE profile or no device in mProfileDeviceMap. Do nothing."); 353 return; 354 } 355 LocalBluetoothProfile vcp = mProfileManager.getVolumeControlProfile(); 356 LocalBluetoothProfile csip = mProfileManager.getCsipSetCoordinatorProfile(); 357 LocalBluetoothProfile a2dp = mProfileManager.getA2dpProfile(); 358 LocalBluetoothProfile headset = mProfileManager.getHeadsetProfile(); 359 360 for (CachedBluetoothDevice leAudioDevice : mProfileDeviceMap.get(profile.toString())) { 361 Log.d(TAG, 362 "User disable LE device: " + leAudioDevice.getDevice().getAnonymizedAddress()); 363 profile.setEnabled(leAudioDevice.getDevice(), false); 364 if (vcp != null) { 365 vcp.setEnabled(leAudioDevice.getDevice(), false); 366 } 367 if (csip != null) { 368 csip.setEnabled(leAudioDevice.getDevice(), false); 369 } 370 } 371 372 enableProfileAfterUserDisablesLeAudio(a2dp); 373 enableProfileAfterUserDisablesLeAudio(headset); 374 } 375 376 /** 377 * When user enable the Le Audio profile, the system needs to do two things. 378 * 1) Disable the A2dp profile and Headset profile for the associated device. The system 379 * can't enable the Le Audio if the A2dp profile and Headset profile are enabled. 380 * 2) Enable the Le Audio profile, VCP and CSIP for each of the Le Audio devices. 381 * 382 * @param profile the LeAudio profile 383 */ enableLeAudioProfile(LocalBluetoothProfile profile)384 private void enableLeAudioProfile(LocalBluetoothProfile profile) { 385 if (profile == null || mProfileDeviceMap.get(profile.toString()) == null) { 386 Log.e(TAG, "There is no the LE profile or no device in mProfileDeviceMap. Do nothing."); 387 return; 388 } 389 LocalBluetoothProfile a2dp = mProfileManager.getA2dpProfile(); 390 LocalBluetoothProfile headset = mProfileManager.getHeadsetProfile(); 391 LocalBluetoothProfile vcp = mProfileManager.getVolumeControlProfile(); 392 LocalBluetoothProfile csip = mProfileManager.getCsipSetCoordinatorProfile(); 393 394 disableProfileBeforeUserEnablesLeAudio(a2dp); 395 disableProfileBeforeUserEnablesLeAudio(headset); 396 397 for (CachedBluetoothDevice leAudioDevice : mProfileDeviceMap.get(profile.toString())) { 398 Log.d(TAG, 399 "User enable LE device: " + leAudioDevice.getDevice().getAnonymizedAddress()); 400 profile.setEnabled(leAudioDevice.getDevice(), true); 401 if (vcp != null) { 402 vcp.setEnabled(leAudioDevice.getDevice(), true); 403 } 404 if (csip != null) { 405 csip.setEnabled(leAudioDevice.getDevice(), true); 406 } 407 } 408 } 409 disableProfileBeforeUserEnablesLeAudio(LocalBluetoothProfile profile)410 private void disableProfileBeforeUserEnablesLeAudio(LocalBluetoothProfile profile) { 411 if (profile != null && mProfileDeviceMap.get(profile.toString()) != null) { 412 Log.d(TAG, "Disable " + profile.toString() + " before user enables LE"); 413 for (CachedBluetoothDevice profileDevice : mProfileDeviceMap.get(profile.toString())) { 414 if (profile.isEnabled(profileDevice.getDevice())) { 415 profile.setEnabled(profileDevice.getDevice(), false); 416 } else { 417 Log.d(TAG, "The " + profile.toString() + " profile is disabled. Do nothing."); 418 } 419 } 420 } 421 } 422 enableProfileAfterUserDisablesLeAudio(LocalBluetoothProfile profile)423 private void enableProfileAfterUserDisablesLeAudio(LocalBluetoothProfile profile) { 424 if (profile != null && mProfileDeviceMap.get(profile.toString()) != null) { 425 Log.d(TAG, "enable " + profile.toString() + "after user disables LE"); 426 for (CachedBluetoothDevice profileDevice : mProfileDeviceMap.get(profile.toString())) { 427 if (!profile.isEnabled(profileDevice.getDevice())) { 428 profile.setEnabled(profileDevice.getDevice(), true); 429 } else { 430 Log.d(TAG, "The " + profile.toString() + " profile is enabled. Do nothing."); 431 } 432 } 433 } 434 } 435 436 /** 437 * This is a helper method to be called after adding a Preference for a profile. If that 438 * profile happened to be A2dp and the device supports high quality audio, it will add a 439 * separate preference for controlling whether to actually use high quality audio. 440 * 441 * @param profile the profile just added 442 */ maybeAddHighQualityAudioPref(LocalBluetoothProfile profile)443 private void maybeAddHighQualityAudioPref(LocalBluetoothProfile profile) { 444 if (!(profile instanceof A2dpProfile)) { 445 return; 446 } 447 BluetoothDevice device = mCachedDevice.getDevice(); 448 A2dpProfile a2dp = (A2dpProfile) profile; 449 if (a2dp.isProfileReady() && a2dp.supportsHighQualityAudio(device)) { 450 SwitchPreference highQualityAudioPref = new SwitchPreference( 451 mProfilesContainer.getContext()); 452 highQualityAudioPref.setKey(HIGH_QUALITY_AUDIO_PREF_TAG); 453 highQualityAudioPref.setVisible(false); 454 highQualityAudioPref.setOnPreferenceClickListener(clickedPref -> { 455 boolean enable = ((SwitchPreference) clickedPref).isChecked(); 456 a2dp.setHighQualityAudioEnabled(mCachedDevice.getDevice(), enable); 457 return true; 458 }); 459 mProfilesContainer.addPreference(highQualityAudioPref); 460 } 461 } 462 463 @Override onPause()464 public void onPause() { 465 for (CachedBluetoothDevice item : mAllOfCachedDevices) { 466 item.unregisterCallback(this); 467 } 468 mProfileManager.removeServiceListener(this); 469 } 470 471 @Override onResume()472 public void onResume() { 473 for (CachedBluetoothDevice item : mAllOfCachedDevices) { 474 item.registerCallback(this); 475 } 476 mProfileManager.addServiceListener(this); 477 } 478 479 @Override onDeviceAttributesChanged()480 public void onDeviceAttributesChanged() { 481 for (CachedBluetoothDevice item : mAllOfCachedDevices) { 482 item.unregisterCallback(this); 483 } 484 mAllOfCachedDevices = getAllOfCachedBluetoothDevices(); 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