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.support.annotation.VisibleForTesting; 23 import android.support.v14.preference.PreferenceFragment; 24 import android.support.v14.preference.SwitchPreference; 25 import android.support.v7.preference.Preference; 26 import android.support.v7.preference.PreferenceCategory; 27 import android.support.v7.preference.PreferenceScreen; 28 import android.text.TextUtils; 29 30 import com.android.settingslib.bluetooth.A2dpProfile; 31 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 32 import com.android.settingslib.bluetooth.LocalBluetoothManager; 33 import com.android.settingslib.bluetooth.LocalBluetoothProfile; 34 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 35 import com.android.settingslib.bluetooth.MapProfile; 36 import com.android.settingslib.bluetooth.PanProfile; 37 import com.android.settingslib.bluetooth.PbapServerProfile; 38 import com.android.settingslib.core.lifecycle.Lifecycle; 39 40 import java.util.List; 41 42 /** 43 * This class adds switches for toggling the individual profiles that a Bluetooth device 44 * supports, such as "Phone audio", "Media audio", "Contact sharing", etc. 45 */ 46 public class BluetoothDetailsProfilesController extends BluetoothDetailsController 47 implements Preference.OnPreferenceClickListener { 48 private static final String KEY_PROFILES_GROUP = "bluetooth_profiles"; 49 50 @VisibleForTesting 51 static final String HIGH_QUALITY_AUDIO_PREF_TAG = "A2dpProfileHighQualityAudio"; 52 53 private LocalBluetoothManager mManager; 54 private LocalBluetoothProfileManager mProfileManager; 55 private CachedBluetoothDevice mCachedDevice; 56 private PreferenceCategory mProfilesContainer; 57 58 public BluetoothDetailsProfilesController(Context context, PreferenceFragment fragment, 59 LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle) { 60 super(context, fragment, device, lifecycle); 61 mManager = manager; 62 mProfileManager = mManager.getProfileManager(); 63 mCachedDevice = device; 64 lifecycle.addObserver(this); 65 } 66 67 @Override 68 protected void init(PreferenceScreen screen) { 69 mProfilesContainer = (PreferenceCategory)screen.findPreference(getPreferenceKey()); 70 // Call refresh here even though it will get called later in onResume, to avoid the 71 // list of switches appearing to "pop" into the page. 72 refresh(); 73 } 74 75 /** 76 * Creates a switch preference for the particular profile. 77 * 78 * @param context The context to use when creating the SwitchPreference 79 * @param profile The profile for which the preference controls. 80 * @return A preference that allows the user to choose whether this profile 81 * will be connected to. 82 */ 83 private SwitchPreference createProfilePreference(Context context, 84 LocalBluetoothProfile profile) { 85 SwitchPreference pref = new SwitchPreference(context); 86 pref.setKey(profile.toString()); 87 pref.setTitle(profile.getNameResource(mCachedDevice.getDevice())); 88 pref.setOnPreferenceClickListener(this); 89 return pref; 90 } 91 92 /** 93 * Refreshes the state for an existing SwitchPreference for a profile. 94 */ 95 private void refreshProfilePreference(SwitchPreference profilePref, 96 LocalBluetoothProfile profile) { 97 BluetoothDevice device = mCachedDevice.getDevice(); 98 profilePref.setEnabled(!mCachedDevice.isBusy()); 99 if (profile instanceof MapProfile) { 100 profilePref.setChecked(mCachedDevice.getMessagePermissionChoice() 101 == CachedBluetoothDevice.ACCESS_ALLOWED); 102 } else if (profile instanceof PbapServerProfile) { 103 profilePref.setChecked(mCachedDevice.getPhonebookPermissionChoice() 104 == CachedBluetoothDevice.ACCESS_ALLOWED); 105 } else if (profile instanceof PanProfile) { 106 profilePref.setChecked(profile.getConnectionStatus(device) == 107 BluetoothProfile.STATE_CONNECTED); 108 } else { 109 profilePref.setChecked(profile.isPreferred(device)); 110 } 111 112 if (profile instanceof A2dpProfile) { 113 A2dpProfile a2dp = (A2dpProfile) profile; 114 SwitchPreference highQualityPref = (SwitchPreference) mProfilesContainer.findPreference( 115 HIGH_QUALITY_AUDIO_PREF_TAG); 116 if (highQualityPref != null) { 117 if (a2dp.isPreferred(device) && a2dp.supportsHighQualityAudio(device)) { 118 highQualityPref.setVisible(true); 119 highQualityPref.setTitle(a2dp.getHighQualityAudioOptionLabel(device)); 120 highQualityPref.setChecked(a2dp.isHighQualityAudioEnabled(device)); 121 highQualityPref.setEnabled(!mCachedDevice.isBusy()); 122 } else { 123 highQualityPref.setVisible(false); 124 } 125 } 126 } 127 } 128 129 /** 130 * Helper method to enable a profile for a device. 131 */ 132 private void enableProfile(LocalBluetoothProfile profile, BluetoothDevice device, 133 SwitchPreference profilePref) { 134 if (profile instanceof PbapServerProfile) { 135 mCachedDevice.setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED); 136 // We don't need to do the additional steps below for this profile. 137 return; 138 } 139 if (profile instanceof MapProfile) { 140 mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED); 141 } 142 profile.setPreferred(device, true); 143 mCachedDevice.connectProfile(profile); 144 } 145 146 /** 147 * Helper method to disable a profile for a device 148 */ 149 private void disableProfile(LocalBluetoothProfile profile, BluetoothDevice device, 150 SwitchPreference profilePref) { 151 if (profile instanceof PbapServerProfile) { 152 mCachedDevice.setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED); 153 // We don't need to do the additional steps below for this profile. 154 return; 155 } 156 mCachedDevice.disconnect(profile); 157 profile.setPreferred(device, false); 158 if (profile instanceof MapProfile) { 159 mCachedDevice.setMessagePermissionChoice(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 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 BluetoothDevice device = mCachedDevice.getDevice(); 181 if (profilePref.isChecked()) { 182 enableProfile(profile, device, profilePref); 183 } else { 184 disableProfile(profile, device, profilePref); 185 } 186 refreshProfilePreference(profilePref, profile); 187 return true; 188 } 189 190 191 /** 192 * Helper to get the list of connectable and special profiles. 193 */ 194 private List<LocalBluetoothProfile> getProfiles() { 195 List<LocalBluetoothProfile> result = mCachedDevice.getConnectableProfiles(); 196 197 final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); 198 // Only provide PBAP cabability if the client device has requested PBAP. 199 if (pbapPermission != CachedBluetoothDevice.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 = mCachedDevice.getMessagePermissionChoice(); 206 if (mapPermission != CachedBluetoothDevice.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 */ 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.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 /** 241 * Refreshes the state of the switches for all profiles, possibly adding or removing switches as 242 * needed. 243 */ 244 @Override 245 protected void refresh() { 246 for (LocalBluetoothProfile profile : getProfiles()) { 247 SwitchPreference pref = (SwitchPreference) mProfilesContainer.findPreference( 248 profile.toString()); 249 if (pref == null) { 250 pref = createProfilePreference(mProfilesContainer.getContext(), profile); 251 mProfilesContainer.addPreference(pref); 252 maybeAddHighQualityAudioPref(profile); 253 } 254 refreshProfilePreference(pref, profile); 255 } 256 for (LocalBluetoothProfile removedProfile : mCachedDevice.getRemovedProfiles()) { 257 SwitchPreference pref = (SwitchPreference) mProfilesContainer.findPreference( 258 removedProfile.toString()); 259 if (pref != null) { 260 mProfilesContainer.removePreference(pref); 261 } 262 } 263 } 264 265 @Override 266 public String getPreferenceKey() { 267 return KEY_PROFILES_GROUP; 268 } 269 } 270