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