1 /* 2 * Copyright (C) 2011 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.settingslib.bluetooth; 18 19 import android.bluetooth.BluetoothA2dp; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothClass; 22 import android.bluetooth.BluetoothCodecConfig; 23 import android.bluetooth.BluetoothDevice; 24 import android.bluetooth.BluetoothProfile; 25 import android.bluetooth.BluetoothUuid; 26 import android.content.Context; 27 import android.os.ParcelUuid; 28 import android.util.Log; 29 30 import com.android.settingslib.R; 31 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.List; 35 36 public class A2dpProfile implements LocalBluetoothProfile { 37 private static final String TAG = "A2dpProfile"; 38 39 private Context mContext; 40 41 private BluetoothA2dp mService; 42 private boolean mIsProfileReady; 43 44 private final CachedBluetoothDeviceManager mDeviceManager; 45 46 static final ParcelUuid[] SINK_UUIDS = { 47 BluetoothUuid.AudioSink, 48 BluetoothUuid.AdvAudioDist, 49 }; 50 51 static final String NAME = "A2DP"; 52 private final LocalBluetoothProfileManager mProfileManager; 53 54 // Order of this profile in device profiles list 55 private static final int ORDINAL = 1; 56 57 // These callbacks run on the main thread. 58 private final class A2dpServiceListener 59 implements BluetoothProfile.ServiceListener { 60 onServiceConnected(int profile, BluetoothProfile proxy)61 public void onServiceConnected(int profile, BluetoothProfile proxy) { 62 mService = (BluetoothA2dp) proxy; 63 // We just bound to the service, so refresh the UI for any connected A2DP devices. 64 List<BluetoothDevice> deviceList = mService.getConnectedDevices(); 65 while (!deviceList.isEmpty()) { 66 BluetoothDevice nextDevice = deviceList.remove(0); 67 CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice); 68 // we may add a new device here, but generally this should not happen 69 if (device == null) { 70 Log.w(TAG, "A2dpProfile found new device: " + nextDevice); 71 device = mDeviceManager.addDevice(nextDevice); 72 } 73 device.onProfileStateChanged(A2dpProfile.this, BluetoothProfile.STATE_CONNECTED); 74 device.refresh(); 75 } 76 mIsProfileReady=true; 77 mProfileManager.callServiceConnectedListeners(); 78 } 79 onServiceDisconnected(int profile)80 public void onServiceDisconnected(int profile) { 81 mIsProfileReady=false; 82 } 83 } 84 isProfileReady()85 public boolean isProfileReady() { 86 return mIsProfileReady; 87 } 88 89 @Override getProfileId()90 public int getProfileId() { 91 return BluetoothProfile.A2DP; 92 } 93 A2dpProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager)94 A2dpProfile(Context context, CachedBluetoothDeviceManager deviceManager, 95 LocalBluetoothProfileManager profileManager) { 96 mContext = context; 97 mDeviceManager = deviceManager; 98 mProfileManager = profileManager; 99 BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new A2dpServiceListener(), 100 BluetoothProfile.A2DP); 101 } 102 accessProfileEnabled()103 public boolean accessProfileEnabled() { 104 return true; 105 } 106 isAutoConnectable()107 public boolean isAutoConnectable() { 108 return true; 109 } 110 111 /** 112 * Get A2dp devices matching connection states{ 113 * @code BluetoothProfile.STATE_CONNECTED, 114 * @code BluetoothProfile.STATE_CONNECTING, 115 * @code BluetoothProfile.STATE_DISCONNECTING} 116 * 117 * @return Matching device list 118 */ getConnectedDevices()119 public List<BluetoothDevice> getConnectedDevices() { 120 return getDevicesByStates(new int[] { 121 BluetoothProfile.STATE_CONNECTED, 122 BluetoothProfile.STATE_CONNECTING, 123 BluetoothProfile.STATE_DISCONNECTING}); 124 } 125 126 /** 127 * Get A2dp devices matching connection states{ 128 * @code BluetoothProfile.STATE_DISCONNECTED, 129 * @code BluetoothProfile.STATE_CONNECTED, 130 * @code BluetoothProfile.STATE_CONNECTING, 131 * @code BluetoothProfile.STATE_DISCONNECTING} 132 * 133 * @return Matching device list 134 */ getConnectableDevices()135 public List<BluetoothDevice> getConnectableDevices() { 136 return getDevicesByStates(new int[] { 137 BluetoothProfile.STATE_DISCONNECTED, 138 BluetoothProfile.STATE_CONNECTED, 139 BluetoothProfile.STATE_CONNECTING, 140 BluetoothProfile.STATE_DISCONNECTING}); 141 } 142 getDevicesByStates(int[] states)143 private List<BluetoothDevice> getDevicesByStates(int[] states) { 144 if (mService == null) { 145 return new ArrayList<BluetoothDevice>(0); 146 } 147 return mService.getDevicesMatchingConnectionStates(states); 148 } 149 connect(BluetoothDevice device)150 public boolean connect(BluetoothDevice device) { 151 if (mService == null) return false; 152 return mService.connect(device); 153 } 154 disconnect(BluetoothDevice device)155 public boolean disconnect(BluetoothDevice device) { 156 if (mService == null) return false; 157 // Downgrade priority as user is disconnecting the headset. 158 if (mService.getPriority(device) > BluetoothProfile.PRIORITY_ON){ 159 mService.setPriority(device, BluetoothProfile.PRIORITY_ON); 160 } 161 return mService.disconnect(device); 162 } 163 getConnectionStatus(BluetoothDevice device)164 public int getConnectionStatus(BluetoothDevice device) { 165 if (mService == null) { 166 return BluetoothProfile.STATE_DISCONNECTED; 167 } 168 return mService.getConnectionState(device); 169 } 170 setActiveDevice(BluetoothDevice device)171 public boolean setActiveDevice(BluetoothDevice device) { 172 if (mService == null) return false; 173 return mService.setActiveDevice(device); 174 } 175 getActiveDevice()176 public BluetoothDevice getActiveDevice() { 177 if (mService == null) return null; 178 return mService.getActiveDevice(); 179 } 180 isPreferred(BluetoothDevice device)181 public boolean isPreferred(BluetoothDevice device) { 182 if (mService == null) return false; 183 return mService.getPriority(device) > BluetoothProfile.PRIORITY_OFF; 184 } 185 getPreferred(BluetoothDevice device)186 public int getPreferred(BluetoothDevice device) { 187 if (mService == null) return BluetoothProfile.PRIORITY_OFF; 188 return mService.getPriority(device); 189 } 190 setPreferred(BluetoothDevice device, boolean preferred)191 public void setPreferred(BluetoothDevice device, boolean preferred) { 192 if (mService == null) return; 193 if (preferred) { 194 if (mService.getPriority(device) < BluetoothProfile.PRIORITY_ON) { 195 mService.setPriority(device, BluetoothProfile.PRIORITY_ON); 196 } 197 } else { 198 mService.setPriority(device, BluetoothProfile.PRIORITY_OFF); 199 } 200 } isA2dpPlaying()201 boolean isA2dpPlaying() { 202 if (mService == null) return false; 203 List<BluetoothDevice> sinks = mService.getConnectedDevices(); 204 for (BluetoothDevice device : sinks) { 205 if (mService.isA2dpPlaying(device)) { 206 return true; 207 } 208 } 209 return false; 210 } 211 supportsHighQualityAudio(BluetoothDevice device)212 public boolean supportsHighQualityAudio(BluetoothDevice device) { 213 int support = mService.supportsOptionalCodecs(device); 214 return support == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED; 215 } 216 isHighQualityAudioEnabled(BluetoothDevice device)217 public boolean isHighQualityAudioEnabled(BluetoothDevice device) { 218 int enabled = mService.getOptionalCodecsEnabled(device); 219 if (enabled != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN) { 220 return enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED; 221 } else if (getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED && 222 supportsHighQualityAudio(device)) { 223 // Since we don't have a stored preference and the device isn't connected, just return 224 // true since the default behavior when the device gets connected in the future would be 225 // to have optional codecs enabled. 226 return true; 227 } 228 BluetoothCodecConfig codecConfig = null; 229 if (mService.getCodecStatus(device) != null) { 230 codecConfig = mService.getCodecStatus(device).getCodecConfig(); 231 } 232 if (codecConfig != null) { 233 return !codecConfig.isMandatoryCodec(); 234 } else { 235 return false; 236 } 237 } 238 setHighQualityAudioEnabled(BluetoothDevice device, boolean enabled)239 public void setHighQualityAudioEnabled(BluetoothDevice device, boolean enabled) { 240 int prefValue = enabled 241 ? BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED 242 : BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED; 243 mService.setOptionalCodecsEnabled(device, prefValue); 244 if (getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED) { 245 return; 246 } 247 if (enabled) { 248 mService.enableOptionalCodecs(device); 249 } else { 250 mService.disableOptionalCodecs(device); 251 } 252 } 253 getHighQualityAudioOptionLabel(BluetoothDevice device)254 public String getHighQualityAudioOptionLabel(BluetoothDevice device) { 255 int unknownCodecId = R.string.bluetooth_profile_a2dp_high_quality_unknown_codec; 256 if (!supportsHighQualityAudio(device) 257 || getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED) { 258 return mContext.getString(unknownCodecId); 259 } 260 // We want to get the highest priority codec, since that's the one that will be used with 261 // this device, and see if it is high-quality (ie non-mandatory). 262 BluetoothCodecConfig[] selectable = null; 263 if (mService.getCodecStatus(device) != null) { 264 selectable = mService.getCodecStatus(device).getCodecsSelectableCapabilities(); 265 // To get the highest priority, we sort in reverse. 266 Arrays.sort(selectable, 267 (a, b) -> { 268 return b.getCodecPriority() - a.getCodecPriority(); 269 }); 270 } 271 272 final BluetoothCodecConfig codecConfig = (selectable == null || selectable.length < 1) 273 ? null : selectable[0]; 274 final int codecType = (codecConfig == null || codecConfig.isMandatoryCodec()) 275 ? BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID : codecConfig.getCodecType(); 276 277 int index = -1; 278 switch (codecType) { 279 case BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC: 280 index = 1; 281 break; 282 case BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC: 283 index = 2; 284 break; 285 case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX: 286 index = 3; 287 break; 288 case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD: 289 index = 4; 290 break; 291 case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC: 292 index = 5; 293 break; 294 } 295 296 if (index < 0) { 297 return mContext.getString(unknownCodecId); 298 } 299 return mContext.getString(R.string.bluetooth_profile_a2dp_high_quality, 300 mContext.getResources().getStringArray(R.array.bluetooth_a2dp_codec_titles)[index]); 301 } 302 toString()303 public String toString() { 304 return NAME; 305 } 306 getOrdinal()307 public int getOrdinal() { 308 return ORDINAL; 309 } 310 getNameResource(BluetoothDevice device)311 public int getNameResource(BluetoothDevice device) { 312 return R.string.bluetooth_profile_a2dp; 313 } 314 getSummaryResourceForDevice(BluetoothDevice device)315 public int getSummaryResourceForDevice(BluetoothDevice device) { 316 int state = getConnectionStatus(device); 317 switch (state) { 318 case BluetoothProfile.STATE_DISCONNECTED: 319 return R.string.bluetooth_a2dp_profile_summary_use_for; 320 321 case BluetoothProfile.STATE_CONNECTED: 322 return R.string.bluetooth_a2dp_profile_summary_connected; 323 324 default: 325 return BluetoothUtils.getConnectionStateSummary(state); 326 } 327 } 328 getDrawableResource(BluetoothClass btClass)329 public int getDrawableResource(BluetoothClass btClass) { 330 return com.android.internal.R.drawable.ic_bt_headphones_a2dp; 331 } 332 finalize()333 protected void finalize() { 334 Log.d(TAG, "finalize()"); 335 if (mService != null) { 336 try { 337 BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.A2DP, 338 mService); 339 mService = null; 340 }catch (Throwable t) { 341 Log.w(TAG, "Error cleaning up A2DP proxy", t); 342 } 343 } 344 } 345 } 346