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