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 17package com.android.settingslib.bluetooth; 18 19import android.bluetooth.BluetoothA2dp; 20import android.bluetooth.BluetoothAdapter; 21import android.bluetooth.BluetoothClass; 22import android.bluetooth.BluetoothCodecConfig; 23import android.bluetooth.BluetoothCodecStatus; 24import android.bluetooth.BluetoothDevice; 25import android.bluetooth.BluetoothProfile; 26import android.bluetooth.BluetoothUuid; 27import android.content.Context; 28import android.os.ParcelUuid; 29import android.util.Log; 30 31import com.android.internal.annotations.VisibleForTesting; 32import com.android.settingslib.R; 33 34import java.util.ArrayList; 35import java.util.Arrays; 36import java.util.List; 37 38public 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 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 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 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 110 void setWrapperFactory(BluetoothA2dpWrapper.Factory factory) { 111 mWrapperFactory = factory; 112 } 113 114 public boolean isConnectable() { 115 return true; 116 } 117 118 public boolean isAutoConnectable() { 119 return true; 120 } 121 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 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 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 154 public int getConnectionStatus(BluetoothDevice device) { 155 if (mService == null) { 156 return BluetoothProfile.STATE_DISCONNECTED; 157 } 158 return mService.getConnectionState(device); 159 } 160 161 public boolean isPreferred(BluetoothDevice device) { 162 if (mService == null) return false; 163 return mService.getPriority(device) > BluetoothProfile.PRIORITY_OFF; 164 } 165 166 public int getPreferred(BluetoothDevice device) { 167 if (mService == null) return BluetoothProfile.PRIORITY_OFF; 168 return mService.getPriority(device); 169 } 170 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 } 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 192 public boolean supportsHighQualityAudio(BluetoothDevice device) { 193 int support = mServiceWrapper.supportsOptionalCodecs(device); 194 return support == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED; 195 } 196 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 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 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 if (selectable == null || selectable.length < 1 || selectable[0].isMandatoryCodec()) { 252 return mContext.getString(unknownCodecId); 253 } 254 return mContext.getString(R.string.bluetooth_profile_a2dp_high_quality, 255 selectable[0].getCodecName()); 256 } 257 258 public String toString() { 259 return NAME; 260 } 261 262 public int getOrdinal() { 263 return ORDINAL; 264 } 265 266 public int getNameResource(BluetoothDevice device) { 267 return R.string.bluetooth_profile_a2dp; 268 } 269 270 public int getSummaryResourceForDevice(BluetoothDevice device) { 271 int state = getConnectionStatus(device); 272 switch (state) { 273 case BluetoothProfile.STATE_DISCONNECTED: 274 return R.string.bluetooth_a2dp_profile_summary_use_for; 275 276 case BluetoothProfile.STATE_CONNECTED: 277 return R.string.bluetooth_a2dp_profile_summary_connected; 278 279 default: 280 return Utils.getConnectionStateSummary(state); 281 } 282 } 283 284 public int getDrawableResource(BluetoothClass btClass) { 285 return R.drawable.ic_bt_headphones_a2dp; 286 } 287 288 protected void finalize() { 289 if (V) Log.d(TAG, "finalize()"); 290 if (mService != null) { 291 try { 292 BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.A2DP, 293 mService); 294 mService = null; 295 }catch (Throwable t) { 296 Log.w(TAG, "Error cleaning up A2DP proxy", t); 297 } 298 } 299 } 300} 301