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