• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
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 = 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 
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 
97     @Override
getProfileId()98     public int getProfileId() {
99         return BluetoothProfile.A2DP;
100     }
101 
A2dpProfile(Context context, LocalBluetoothAdapter adapter, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager)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
setBluetoothA2dpWrapper(BluetoothA2dpWrapper wrapper)114     void setBluetoothA2dpWrapper(BluetoothA2dpWrapper wrapper) {
115         mServiceWrapper = wrapper;
116     }
117 
isConnectable()118     public boolean isConnectable() {
119         return true;
120     }
121 
isAutoConnectable()122     public boolean isAutoConnectable() {
123         return true;
124     }
125 
getConnectedDevices()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 
connect(BluetoothDevice device)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 
disconnect(BluetoothDevice device)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 
getConnectionStatus(BluetoothDevice device)162     public int getConnectionStatus(BluetoothDevice device) {
163         if (mService == null) {
164             return BluetoothProfile.STATE_DISCONNECTED;
165         }
166         return mService.getConnectionState(device);
167     }
168 
setActiveDevice(BluetoothDevice device)169     public boolean setActiveDevice(BluetoothDevice device) {
170         if (mService == null) return false;
171         return mService.setActiveDevice(device);
172     }
173 
getActiveDevice()174     public BluetoothDevice getActiveDevice() {
175         if (mService == null) return null;
176         return mService.getActiveDevice();
177     }
178 
isPreferred(BluetoothDevice device)179     public boolean isPreferred(BluetoothDevice device) {
180         if (mService == null) return false;
181         return mService.getPriority(device) > BluetoothProfile.PRIORITY_OFF;
182     }
183 
getPreferred(BluetoothDevice device)184     public int getPreferred(BluetoothDevice device) {
185         if (mService == null) return BluetoothProfile.PRIORITY_OFF;
186         return mService.getPriority(device);
187     }
188 
setPreferred(BluetoothDevice device, boolean preferred)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     }
isA2dpPlaying()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 
supportsHighQualityAudio(BluetoothDevice device)210     public boolean supportsHighQualityAudio(BluetoothDevice device) {
211         int support = mServiceWrapper.supportsOptionalCodecs(device);
212         return support == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED;
213     }
214 
isHighQualityAudioEnabled(BluetoothDevice device)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 
setHighQualityAudioEnabled(BluetoothDevice device, boolean enabled)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 
getHighQualityAudioOptionLabel(BluetoothDevice device)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 
toString()301     public String toString() {
302         return NAME;
303     }
304 
getOrdinal()305     public int getOrdinal() {
306         return ORDINAL;
307     }
308 
getNameResource(BluetoothDevice device)309     public int getNameResource(BluetoothDevice device) {
310         return R.string.bluetooth_profile_a2dp;
311     }
312 
getSummaryResourceForDevice(BluetoothDevice device)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 
getDrawableResource(BluetoothClass btClass)327     public int getDrawableResource(BluetoothClass btClass) {
328         return R.drawable.ic_bt_headphones_a2dp;
329     }
330 
finalize()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