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