blob: 785dd561d1b7a9163780019afd2c89db48a38625 [file] [log] [blame]
Jason Monk7ce96b92015-02-02 11:27:58 -05001/*
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;
Antony Sargent374d2592017-04-20 11:23:34 -070022import android.bluetooth.BluetoothCodecConfig;
Jason Monk7ce96b92015-02-02 11:27:58 -050023import android.bluetooth.BluetoothDevice;
24import android.bluetooth.BluetoothProfile;
25import android.bluetooth.BluetoothUuid;
26import android.content.Context;
27import android.os.ParcelUuid;
28import android.util.Log;
29
30import com.android.settingslib.R;
31
32import java.util.ArrayList;
Antony Sargent374d2592017-04-20 11:23:34 -070033import java.util.Arrays;
Jason Monk7ce96b92015-02-02 11:27:58 -050034import java.util.List;
35
Antony Sargent374d2592017-04-20 11:23:34 -070036public class A2dpProfile implements LocalBluetoothProfile {
Jason Monk7ce96b92015-02-02 11:27:58 -050037 private static final String TAG = "A2dpProfile";
Jason Monk7ce96b92015-02-02 11:27:58 -050038
Antony Sargent374d2592017-04-20 11:23:34 -070039 private Context mContext;
40
Jason Monk7ce96b92015-02-02 11:27:58 -050041 private BluetoothA2dp mService;
42 private boolean mIsProfileReady;
43
Jason Monk7ce96b92015-02-02 11:27:58 -050044 private final CachedBluetoothDeviceManager mDeviceManager;
45
46 static final ParcelUuid[] SINK_UUIDS = {
47 BluetoothUuid.AudioSink,
48 BluetoothUuid.AdvAudioDist,
49 };
50
51 static final String NAME = "A2DP";
52 private final LocalBluetoothProfileManager mProfileManager;
53
54 // Order of this profile in device profiles list
55 private static final int ORDINAL = 1;
56
57 // These callbacks run on the main thread.
58 private final class A2dpServiceListener
59 implements BluetoothProfile.ServiceListener {
60
61 public void onServiceConnected(int profile, BluetoothProfile proxy) {
Jason Monk7ce96b92015-02-02 11:27:58 -050062 mService = (BluetoothA2dp) proxy;
63 // We just bound to the service, so refresh the UI for any connected A2DP devices.
64 List<BluetoothDevice> deviceList = mService.getConnectedDevices();
65 while (!deviceList.isEmpty()) {
66 BluetoothDevice nextDevice = deviceList.remove(0);
67 CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
68 // we may add a new device here, but generally this should not happen
69 if (device == null) {
70 Log.w(TAG, "A2dpProfile found new device: " + nextDevice);
timhypengf64f6902018-07-31 15:40:15 +080071 device = mDeviceManager.addDevice(nextDevice);
Jason Monk7ce96b92015-02-02 11:27:58 -050072 }
73 device.onProfileStateChanged(A2dpProfile.this, BluetoothProfile.STATE_CONNECTED);
74 device.refresh();
75 }
76 mIsProfileReady=true;
hughchenbd0dd492019-01-08 14:34:10 +080077 mProfileManager.callServiceConnectedListeners();
Jason Monk7ce96b92015-02-02 11:27:58 -050078 }
79
80 public void onServiceDisconnected(int profile) {
Jason Monk7ce96b92015-02-02 11:27:58 -050081 mIsProfileReady=false;
82 }
83 }
84
85 public boolean isProfileReady() {
86 return mIsProfileReady;
87 }
88
ryanywlin44de3a02018-05-02 15:15:37 +080089 @Override
90 public int getProfileId() {
91 return BluetoothProfile.A2DP;
92 }
93
timhypengf64f6902018-07-31 15:40:15 +080094 A2dpProfile(Context context, CachedBluetoothDeviceManager deviceManager,
Jason Monk7ce96b92015-02-02 11:27:58 -050095 LocalBluetoothProfileManager profileManager) {
Antony Sargent374d2592017-04-20 11:23:34 -070096 mContext = context;
Jason Monk7ce96b92015-02-02 11:27:58 -050097 mDeviceManager = deviceManager;
98 mProfileManager = profileManager;
timhypengf64f6902018-07-31 15:40:15 +080099 BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new A2dpServiceListener(),
Jason Monk7ce96b92015-02-02 11:27:58 -0500100 BluetoothProfile.A2DP);
Antony Sargent374d2592017-04-20 11:23:34 -0700101 }
102
Leon Liao3b5e6bd2018-09-18 12:45:45 +0800103 public boolean accessProfileEnabled() {
Jason Monk7ce96b92015-02-02 11:27:58 -0500104 return true;
105 }
106
107 public boolean isAutoConnectable() {
108 return true;
109 }
110
timhypenge54887b2019-02-27 15:01:31 +0800111 /**
112 * Get A2dp devices matching connection states{
113 * @code BluetoothProfile.STATE_CONNECTED,
114 * @code BluetoothProfile.STATE_CONNECTING,
115 * @code BluetoothProfile.STATE_DISCONNECTING}
116 *
117 * @return Matching device list
118 */
Jason Monk7ce96b92015-02-02 11:27:58 -0500119 public List<BluetoothDevice> getConnectedDevices() {
timhypenge54887b2019-02-27 15:01:31 +0800120 return getDevicesByStates(new int[] {
121 BluetoothProfile.STATE_CONNECTED,
122 BluetoothProfile.STATE_CONNECTING,
123 BluetoothProfile.STATE_DISCONNECTING});
124 }
125
126 /**
127 * Get A2dp devices matching connection states{
128 * @code BluetoothProfile.STATE_DISCONNECTED,
129 * @code BluetoothProfile.STATE_CONNECTED,
130 * @code BluetoothProfile.STATE_CONNECTING,
131 * @code BluetoothProfile.STATE_DISCONNECTING}
132 *
133 * @return Matching device list
134 */
135 public List<BluetoothDevice> getConnectableDevices() {
136 return getDevicesByStates(new int[] {
137 BluetoothProfile.STATE_DISCONNECTED,
138 BluetoothProfile.STATE_CONNECTED,
139 BluetoothProfile.STATE_CONNECTING,
140 BluetoothProfile.STATE_DISCONNECTING});
141 }
142
143 private List<BluetoothDevice> getDevicesByStates(int[] states) {
144 if (mService == null) {
145 return new ArrayList<BluetoothDevice>(0);
146 }
147 return mService.getDevicesMatchingConnectionStates(states);
Jason Monk7ce96b92015-02-02 11:27:58 -0500148 }
149
150 public boolean connect(BluetoothDevice device) {
151 if (mService == null) return false;
Jason Monk7ce96b92015-02-02 11:27:58 -0500152 return mService.connect(device);
153 }
154
155 public boolean disconnect(BluetoothDevice device) {
156 if (mService == null) return false;
157 // Downgrade priority as user is disconnecting the headset.
158 if (mService.getPriority(device) > BluetoothProfile.PRIORITY_ON){
159 mService.setPriority(device, BluetoothProfile.PRIORITY_ON);
160 }
161 return mService.disconnect(device);
162 }
163
164 public int getConnectionStatus(BluetoothDevice device) {
165 if (mService == null) {
166 return BluetoothProfile.STATE_DISCONNECTED;
167 }
168 return mService.getConnectionState(device);
169 }
170
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800171 public boolean setActiveDevice(BluetoothDevice device) {
172 if (mService == null) return false;
173 return mService.setActiveDevice(device);
174 }
175
176 public BluetoothDevice getActiveDevice() {
177 if (mService == null) return null;
178 return mService.getActiveDevice();
179 }
180
Jason Monk7ce96b92015-02-02 11:27:58 -0500181 public boolean isPreferred(BluetoothDevice device) {
182 if (mService == null) return false;
183 return mService.getPriority(device) > BluetoothProfile.PRIORITY_OFF;
184 }
185
186 public int getPreferred(BluetoothDevice device) {
187 if (mService == null) return BluetoothProfile.PRIORITY_OFF;
188 return mService.getPriority(device);
189 }
190
191 public void setPreferred(BluetoothDevice device, boolean preferred) {
192 if (mService == null) return;
193 if (preferred) {
194 if (mService.getPriority(device) < BluetoothProfile.PRIORITY_ON) {
195 mService.setPriority(device, BluetoothProfile.PRIORITY_ON);
196 }
197 } else {
198 mService.setPriority(device, BluetoothProfile.PRIORITY_OFF);
199 }
200 }
201 boolean isA2dpPlaying() {
202 if (mService == null) return false;
203 List<BluetoothDevice> sinks = mService.getConnectedDevices();
Pavlin Radoslavov1af33a12018-01-21 02:59:15 -0800204 for (BluetoothDevice device : sinks) {
205 if (mService.isA2dpPlaying(device)) {
Jason Monk7ce96b92015-02-02 11:27:58 -0500206 return true;
207 }
208 }
209 return false;
210 }
211
Antony Sargent374d2592017-04-20 11:23:34 -0700212 public boolean supportsHighQualityAudio(BluetoothDevice device) {
hjchangliaobd8c8fb2018-04-26 15:12:11 +0800213 int support = mService.supportsOptionalCodecs(device);
Antony Sargent374d2592017-04-20 11:23:34 -0700214 return support == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED;
215 }
216
217 public boolean isHighQualityAudioEnabled(BluetoothDevice device) {
hjchangliaobd8c8fb2018-04-26 15:12:11 +0800218 int enabled = mService.getOptionalCodecsEnabled(device);
Antony Sargent374d2592017-04-20 11:23:34 -0700219 if (enabled != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN) {
220 return enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED;
221 } else if (getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED &&
222 supportsHighQualityAudio(device)) {
223 // Since we don't have a stored preference and the device isn't connected, just return
224 // true since the default behavior when the device gets connected in the future would be
225 // to have optional codecs enabled.
226 return true;
227 }
228 BluetoothCodecConfig codecConfig = null;
hjchangliaobd8c8fb2018-04-26 15:12:11 +0800229 if (mService.getCodecStatus(device) != null) {
230 codecConfig = mService.getCodecStatus(device).getCodecConfig();
Antony Sargent374d2592017-04-20 11:23:34 -0700231 }
232 if (codecConfig != null) {
233 return !codecConfig.isMandatoryCodec();
234 } else {
235 return false;
236 }
237 }
238
239 public void setHighQualityAudioEnabled(BluetoothDevice device, boolean enabled) {
240 int prefValue = enabled
241 ? BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED
242 : BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED;
hjchangliaobd8c8fb2018-04-26 15:12:11 +0800243 mService.setOptionalCodecsEnabled(device, prefValue);
Antony Sargent374d2592017-04-20 11:23:34 -0700244 if (getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED) {
245 return;
246 }
247 if (enabled) {
Pavlin Radoslavov502af212018-01-03 19:38:39 -0800248 mService.enableOptionalCodecs(device);
Antony Sargent374d2592017-04-20 11:23:34 -0700249 } else {
Pavlin Radoslavov502af212018-01-03 19:38:39 -0800250 mService.disableOptionalCodecs(device);
Antony Sargent374d2592017-04-20 11:23:34 -0700251 }
252 }
253
254 public String getHighQualityAudioOptionLabel(BluetoothDevice device) {
255 int unknownCodecId = R.string.bluetooth_profile_a2dp_high_quality_unknown_codec;
Justin Klaassencb605752017-08-28 15:39:45 -0700256 if (!supportsHighQualityAudio(device)
257 || getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED) {
Antony Sargent374d2592017-04-20 11:23:34 -0700258 return mContext.getString(unknownCodecId);
259 }
260 // We want to get the highest priority codec, since that's the one that will be used with
261 // this device, and see if it is high-quality (ie non-mandatory).
262 BluetoothCodecConfig[] selectable = null;
hjchangliaobd8c8fb2018-04-26 15:12:11 +0800263 if (mService.getCodecStatus(device) != null) {
264 selectable = mService.getCodecStatus(device).getCodecsSelectableCapabilities();
Antony Sargent374d2592017-04-20 11:23:34 -0700265 // To get the highest priority, we sort in reverse.
266 Arrays.sort(selectable,
267 (a, b) -> {
268 return b.getCodecPriority() - a.getCodecPriority();
269 });
270 }
Justin Klaassencb605752017-08-28 15:39:45 -0700271
272 final BluetoothCodecConfig codecConfig = (selectable == null || selectable.length < 1)
273 ? null : selectable[0];
274 final int codecType = (codecConfig == null || codecConfig.isMandatoryCodec())
275 ? BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID : codecConfig.getCodecType();
276
277 int index = -1;
278 switch (codecType) {
279 case BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC:
280 index = 1;
281 break;
282 case BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC:
283 index = 2;
284 break;
285 case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX:
286 index = 3;
287 break;
288 case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD:
289 index = 4;
290 break;
291 case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC:
292 index = 5;
293 break;
294 }
295
296 if (index < 0) {
Antony Sargent374d2592017-04-20 11:23:34 -0700297 return mContext.getString(unknownCodecId);
298 }
299 return mContext.getString(R.string.bluetooth_profile_a2dp_high_quality,
Justin Klaassencb605752017-08-28 15:39:45 -0700300 mContext.getResources().getStringArray(R.array.bluetooth_a2dp_codec_titles)[index]);
Antony Sargent374d2592017-04-20 11:23:34 -0700301 }
302
Jason Monk7ce96b92015-02-02 11:27:58 -0500303 public String toString() {
304 return NAME;
305 }
306
307 public int getOrdinal() {
308 return ORDINAL;
309 }
310
311 public int getNameResource(BluetoothDevice device) {
312 return R.string.bluetooth_profile_a2dp;
313 }
314
315 public int getSummaryResourceForDevice(BluetoothDevice device) {
316 int state = getConnectionStatus(device);
317 switch (state) {
318 case BluetoothProfile.STATE_DISCONNECTED:
319 return R.string.bluetooth_a2dp_profile_summary_use_for;
320
321 case BluetoothProfile.STATE_CONNECTED:
322 return R.string.bluetooth_a2dp_profile_summary_connected;
323
324 default:
Kunhung Lie7b7cb82018-05-17 11:04:54 +0800325 return BluetoothUtils.getConnectionStateSummary(state);
Jason Monk7ce96b92015-02-02 11:27:58 -0500326 }
327 }
328
329 public int getDrawableResource(BluetoothClass btClass) {
Amin Shaikh10f363b2019-01-24 17:59:49 -0500330 return com.android.internal.R.drawable.ic_bt_headphones_a2dp;
Jason Monk7ce96b92015-02-02 11:27:58 -0500331 }
332
333 protected void finalize() {
Chienyuan97e4b202019-01-03 20:02:27 +0800334 Log.d(TAG, "finalize()");
Jason Monk7ce96b92015-02-02 11:27:58 -0500335 if (mService != null) {
336 try {
337 BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.A2DP,
338 mService);
339 mService = null;
340 }catch (Throwable t) {
341 Log.w(TAG, "Error cleaning up A2DP proxy", t);
342 }
343 }
344 }
345}