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