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