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