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.text.TextUtils; 23 24 import androidx.annotation.VisibleForTesting; 25 import androidx.preference.Preference; 26 import androidx.preference.PreferenceCategory; 27 import androidx.preference.PreferenceFragmentCompat; 28 import androidx.preference.PreferenceScreen; 29 import androidx.preference.SwitchPreference; 30 31 import com.android.settingslib.bluetooth.A2dpProfile; 32 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 33 import com.android.settingslib.bluetooth.LocalBluetoothManager; 34 import com.android.settingslib.bluetooth.LocalBluetoothProfile; 35 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 36 import com.android.settingslib.bluetooth.MapProfile; 37 import com.android.settingslib.bluetooth.PanProfile; 38 import com.android.settingslib.bluetooth.PbapServerProfile; 39 import com.android.settingslib.core.lifecycle.Lifecycle; 40 41 import java.util.List; 42 43 /** 44 * This class adds switches for toggling the individual profiles that a Bluetooth device 45 * supports, such as "Phone audio", "Media audio", "Contact sharing", etc. 46 */ 47 public class BluetoothDetailsProfilesController extends BluetoothDetailsController 48 implements Preference.OnPreferenceClickListener, 49 LocalBluetoothProfileManager.ServiceListener { 50 private static final String KEY_PROFILES_GROUP = "bluetooth_profiles"; 51 52 @VisibleForTesting 53 static final String HIGH_QUALITY_AUDIO_PREF_TAG = "A2dpProfileHighQualityAudio"; 54 55 private LocalBluetoothManager mManager; 56 private LocalBluetoothProfileManager mProfileManager; 57 private CachedBluetoothDevice mCachedDevice; 58 private PreferenceCategory mProfilesContainer; 59 BluetoothDetailsProfilesController(Context context, PreferenceFragmentCompat fragment, LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle)60 public BluetoothDetailsProfilesController(Context context, PreferenceFragmentCompat fragment, 61 LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle) { 62 super(context, fragment, device, lifecycle); 63 mManager = manager; 64 mProfileManager = mManager.getProfileManager(); 65 mCachedDevice = device; 66 lifecycle.addObserver(this); 67 } 68 69 @Override init(PreferenceScreen screen)70 protected void init(PreferenceScreen screen) { 71 mProfilesContainer = (PreferenceCategory)screen.findPreference(getPreferenceKey()); 72 // Call refresh here even though it will get called later in onResume, to avoid the 73 // list of switches appearing to "pop" into the page. 74 refresh(); 75 } 76 77 /** 78 * Creates a switch preference for the particular profile. 79 * 80 * @param context The context to use when creating the SwitchPreference 81 * @param profile The profile for which the preference controls. 82 * @return A preference that allows the user to choose whether this profile 83 * will be connected to. 84 */ createProfilePreference(Context context, LocalBluetoothProfile profile)85 private SwitchPreference createProfilePreference(Context context, 86 LocalBluetoothProfile profile) { 87 SwitchPreference pref = new SwitchPreference(context); 88 pref.setKey(profile.toString()); 89 pref.setTitle(profile.getNameResource(mCachedDevice.getDevice())); 90 pref.setOnPreferenceClickListener(this); 91 pref.setOrder(profile.getOrdinal()); 92 return pref; 93 } 94 95 /** 96 * Refreshes the state for an existing SwitchPreference for a profile. 97 */ refreshProfilePreference(SwitchPreference profilePref, LocalBluetoothProfile profile)98 private void refreshProfilePreference(SwitchPreference profilePref, 99 LocalBluetoothProfile profile) { 100 BluetoothDevice device = mCachedDevice.getDevice(); 101 profilePref.setEnabled(!mCachedDevice.isBusy()); 102 if (profile instanceof MapProfile) { 103 profilePref.setChecked(device.getMessageAccessPermission() 104 == BluetoothDevice.ACCESS_ALLOWED); 105 } else if (profile instanceof PbapServerProfile) { 106 profilePref.setChecked(device.getPhonebookAccessPermission() 107 == BluetoothDevice.ACCESS_ALLOWED); 108 } else if (profile instanceof PanProfile) { 109 profilePref.setChecked(profile.getConnectionStatus(device) == 110 BluetoothProfile.STATE_CONNECTED); 111 } else { 112 profilePref.setChecked(profile.isPreferred(device)); 113 } 114 115 if (profile instanceof A2dpProfile) { 116 A2dpProfile a2dp = (A2dpProfile) profile; 117 SwitchPreference highQualityPref = (SwitchPreference) mProfilesContainer.findPreference( 118 HIGH_QUALITY_AUDIO_PREF_TAG); 119 if (highQualityPref != null) { 120 if (a2dp.isPreferred(device) && a2dp.supportsHighQualityAudio(device)) { 121 highQualityPref.setVisible(true); 122 highQualityPref.setTitle(a2dp.getHighQualityAudioOptionLabel(device)); 123 highQualityPref.setChecked(a2dp.isHighQualityAudioEnabled(device)); 124 highQualityPref.setEnabled(!mCachedDevice.isBusy()); 125 } else { 126 highQualityPref.setVisible(false); 127 } 128 } 129 } 130 } 131 132 /** 133 * Helper method to enable a profile for a device. 134 */ enableProfile(LocalBluetoothProfile profile)135 private void enableProfile(LocalBluetoothProfile profile) { 136 final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); 137 if (profile instanceof PbapServerProfile) { 138 bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 139 // We don't need to do the additional steps below for this profile. 140 return; 141 } 142 if (profile instanceof MapProfile) { 143 bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 144 } 145 profile.setPreferred(bluetoothDevice, true); 146 mCachedDevice.connectProfile(profile); 147 } 148 149 /** 150 * Helper method to disable a profile for a device 151 */ disableProfile(LocalBluetoothProfile profile)152 private void disableProfile(LocalBluetoothProfile profile) { 153 final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); 154 mCachedDevice.disconnect(profile); 155 profile.setPreferred(bluetoothDevice, false); 156 if (profile instanceof MapProfile) { 157 bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); 158 } else if (profile instanceof PbapServerProfile) { 159 bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 160 } 161 } 162 163 /** 164 * When the pref for a bluetooth profile is clicked on, we want to toggle the enabled/disabled 165 * state for that profile. 166 */ 167 @Override onPreferenceClick(Preference preference)168 public boolean onPreferenceClick(Preference preference) { 169 LocalBluetoothProfile profile = mProfileManager.getProfileByName(preference.getKey()); 170 if (profile == null) { 171 // It might be the PbapServerProfile, which is not stored by name. 172 PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); 173 if (TextUtils.equals(preference.getKey(), psp.toString())) { 174 profile = psp; 175 } else { 176 return false; 177 } 178 } 179 SwitchPreference profilePref = (SwitchPreference) preference; 180 if (profilePref.isChecked()) { 181 enableProfile(profile); 182 } else { 183 disableProfile(profile); 184 } 185 refreshProfilePreference(profilePref, profile); 186 return true; 187 } 188 189 190 /** 191 * Helper to get the list of connectable and special profiles. 192 */ getProfiles()193 private List<LocalBluetoothProfile> getProfiles() { 194 List<LocalBluetoothProfile> result = mCachedDevice.getConnectableProfiles(); 195 final BluetoothDevice device = mCachedDevice.getDevice(); 196 197 final int pbapPermission = device.getPhonebookAccessPermission(); 198 // Only provide PBAP cabability if the client device has requested PBAP. 199 if (pbapPermission != BluetoothDevice.ACCESS_UNKNOWN) { 200 final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); 201 result.add(psp); 202 } 203 204 final MapProfile mapProfile = mManager.getProfileManager().getMapProfile(); 205 final int mapPermission = device.getMessageAccessPermission(); 206 if (mapPermission != BluetoothDevice.ACCESS_UNKNOWN) { 207 result.add(mapProfile); 208 } 209 210 return result; 211 } 212 213 /** 214 * This is a helper method to be called after adding a Preference for a profile. If that 215 * profile happened to be A2dp and the device supports high quality audio, it will add a 216 * separate preference for controlling whether to actually use high quality audio. 217 * 218 * @param profile the profile just added 219 */ maybeAddHighQualityAudioPref(LocalBluetoothProfile profile)220 private void maybeAddHighQualityAudioPref(LocalBluetoothProfile profile) { 221 if (!(profile instanceof A2dpProfile)) { 222 return; 223 } 224 BluetoothDevice device = mCachedDevice.getDevice(); 225 A2dpProfile a2dp = (A2dpProfile) profile; 226 if (a2dp.isProfileReady() && a2dp.supportsHighQualityAudio(device)) { 227 SwitchPreference highQualityAudioPref = new SwitchPreference( 228 mProfilesContainer.getContext()); 229 highQualityAudioPref.setKey(HIGH_QUALITY_AUDIO_PREF_TAG); 230 highQualityAudioPref.setVisible(false); 231 highQualityAudioPref.setOnPreferenceClickListener(clickedPref -> { 232 boolean enable = ((SwitchPreference) clickedPref).isChecked(); 233 a2dp.setHighQualityAudioEnabled(mCachedDevice.getDevice(), enable); 234 return true; 235 }); 236 mProfilesContainer.addPreference(highQualityAudioPref); 237 } 238 } 239 240 @Override onPause()241 public void onPause() { 242 super.onPause(); 243 mProfileManager.removeServiceListener(this); 244 } 245 246 @Override onResume()247 public void onResume() { 248 super.onResume(); 249 mProfileManager.addServiceListener(this); 250 } 251 252 @Override onServiceConnected()253 public void onServiceConnected() { 254 refresh(); 255 } 256 257 @Override onServiceDisconnected()258 public void onServiceDisconnected() { 259 refresh(); 260 } 261 262 /** 263 * Refreshes the state of the switches for all profiles, possibly adding or removing switches as 264 * needed. 265 */ 266 @Override refresh()267 protected void refresh() { 268 for (LocalBluetoothProfile profile : getProfiles()) { 269 if (!profile.isProfileReady()) { 270 continue; 271 } 272 SwitchPreference pref = mProfilesContainer.findPreference( 273 profile.toString()); 274 if (pref == null) { 275 pref = createProfilePreference(mProfilesContainer.getContext(), profile); 276 mProfilesContainer.addPreference(pref); 277 maybeAddHighQualityAudioPref(profile); 278 } 279 refreshProfilePreference(pref, profile); 280 } 281 for (LocalBluetoothProfile removedProfile : mCachedDevice.getRemovedProfiles()) { 282 final SwitchPreference pref = mProfilesContainer.findPreference( 283 removedProfile.toString()); 284 if (pref != null) { 285 mProfilesContainer.removePreference(pref); 286 } 287 } 288 } 289 290 @Override getPreferenceKey()291 public String getPreferenceKey() { 292 return KEY_PROFILES_GROUP; 293 } 294 }