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