• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*   Copyright 2021 HIMSA II K/S - www.himsa.com. Represented by EHIMA
2 - www.ehima.com
3 */
4 
5 /* Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.settingslib.bluetooth;
19 
20 import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL;
21 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
22 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
23 
24 import android.annotation.CallbackExecutor;
25 import android.bluetooth.BluetoothAdapter;
26 import android.bluetooth.BluetoothClass;
27 import android.bluetooth.BluetoothCsipSetCoordinator;
28 import android.bluetooth.BluetoothDevice;
29 import android.bluetooth.BluetoothLeAudio;
30 import android.bluetooth.BluetoothProfile;
31 import android.content.Context;
32 import android.os.Build;
33 import android.util.Log;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.annotation.RequiresApi;
38 
39 import com.android.settingslib.R;
40 import com.android.settingslib.flags.Flags;
41 
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.concurrent.ConcurrentHashMap;
45 import java.util.concurrent.Executor;
46 
47 public class LeAudioProfile implements LocalBluetoothProfile {
48     public static final int LEFT_DEVICE_ID = BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT
49             | BluetoothLeAudio.AUDIO_LOCATION_BACK_LEFT
50             | BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_OF_CENTER
51             | BluetoothLeAudio.AUDIO_LOCATION_SIDE_LEFT
52             | BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_LEFT
53             | BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_LEFT
54             | BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_LEFT
55             | BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_LEFT
56             | BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_WIDE
57             | BluetoothLeAudio.AUDIO_LOCATION_LEFT_SURROUND;
58     public static final int RIGHT_DEVICE_ID = BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT
59             | BluetoothLeAudio.AUDIO_LOCATION_BACK_RIGHT
60             | BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_OF_CENTER
61             | BluetoothLeAudio.AUDIO_LOCATION_SIDE_RIGHT
62             | BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_RIGHT
63             | BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_RIGHT
64             | BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_RIGHT
65             | BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_RIGHT
66             | BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_WIDE
67             | BluetoothLeAudio.AUDIO_LOCATION_RIGHT_SURROUND;
68 
69     private static final String TAG = "LeAudioProfile";
70     private static boolean DEBUG = true;
71 
72     private Context mContext;
73 
74     private BluetoothLeAudio mService;
75     private boolean mIsProfileReady;
76 
77     private final CachedBluetoothDeviceManager mDeviceManager;
78 
79     static final String NAME = "LE_AUDIO";
80     private final LocalBluetoothProfileManager mProfileManager;
81     private final BluetoothAdapter mBluetoothAdapter;
82 
83     // Order of this profile in device profiles list
84     private static final int ORDINAL = 1;
85     // Cached callbacks being registered before service is connected.
86     private ConcurrentHashMap<BluetoothLeAudio.Callback, Executor>
87             mCachedCallbackExecutorMap = new ConcurrentHashMap<>();
88 
89 
90     // These callbacks run on the main thread.
91     private final class LeAudioServiceListener implements BluetoothProfile.ServiceListener {
92 
93         @RequiresApi(Build.VERSION_CODES.S)
onServiceConnected(int profile, BluetoothProfile proxy)94         public void onServiceConnected(int profile, BluetoothProfile proxy) {
95             if (DEBUG) {
96                 Log.d(TAG, "Bluetooth service connected");
97             }
98             mService = (BluetoothLeAudio) proxy;
99             // We just bound to the service, so refresh the UI for any connected LeAudio devices.
100             List<BluetoothDevice> deviceList = mService.getConnectedDevices();
101             while (!deviceList.isEmpty()) {
102                 BluetoothDevice nextDevice = deviceList.remove(0);
103                 CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
104                 // we may add a new device here, but generally this should not happen
105                 if (device == null) {
106                     if (DEBUG) {
107                         Log.d(TAG, "LeAudioProfile found new device: " + nextDevice);
108                     }
109                     device = mDeviceManager.addDevice(nextDevice);
110                 }
111                 device.onProfileStateChanged(LeAudioProfile.this, BluetoothProfile.STATE_CONNECTED);
112                 device.refresh();
113             }
114 
115             // Check current list of CachedDevices to see if any are hearing aid devices.
116             mDeviceManager.updateHearingAidsDevices();
117             mProfileManager.callServiceConnectedListeners();
118             if (!mIsProfileReady) {
119                 mIsProfileReady = true;
120                 if (Flags.adoptPrimaryGroupManagementApiV2()) {
121                     if (DEBUG) {
122                         Log.d(
123                                 TAG,
124                                 "onServiceConnected, register mCachedCallbackExecutorMap = "
125                                         + mCachedCallbackExecutorMap);
126                     }
127                     mCachedCallbackExecutorMap.forEach(
128                             (callback, executor) -> registerCallback(executor, callback));
129                 }
130             }
131         }
132 
onServiceDisconnected(int profile)133         public void onServiceDisconnected(int profile) {
134             if (DEBUG) {
135                 Log.d(TAG, "Bluetooth service disconnected");
136             }
137             mProfileManager.callServiceDisconnectedListeners();
138             if (mIsProfileReady) {
139                 mIsProfileReady = false;
140                 if (Flags.adoptPrimaryGroupManagementApiV2()) {
141                     mCachedCallbackExecutorMap.clear();
142                 }
143             }
144         }
145     }
146 
isProfileReady()147     public boolean isProfileReady() {
148         return mIsProfileReady;
149     }
150 
151     @Override
getProfileId()152     public int getProfileId() {
153         return BluetoothProfile.LE_AUDIO;
154     }
155 
LeAudioProfile( Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager)156     LeAudioProfile(
157             Context context,
158             CachedBluetoothDeviceManager deviceManager,
159             LocalBluetoothProfileManager profileManager) {
160         mContext = context;
161         mDeviceManager = deviceManager;
162         mProfileManager = profileManager;
163 
164         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
165         mBluetoothAdapter.getProfileProxy(
166                 context, new LeAudioServiceListener(), BluetoothProfile.LE_AUDIO);
167     }
168 
accessProfileEnabled()169     public boolean accessProfileEnabled() {
170         return true;
171     }
172 
isAutoConnectable()173     public boolean isAutoConnectable() {
174         return true;
175     }
176 
getConnectedDevices()177     public List<BluetoothDevice> getConnectedDevices() {
178         return getDevicesByStates(
179                 new int[] {
180                     BluetoothProfile.STATE_CONNECTED,
181                     BluetoothProfile.STATE_CONNECTING,
182                     BluetoothProfile.STATE_DISCONNECTING
183                 });
184     }
185 
getConnectableDevices()186     public List<BluetoothDevice> getConnectableDevices() {
187         return getDevicesByStates(
188                 new int[] {
189                     BluetoothProfile.STATE_DISCONNECTED,
190                     BluetoothProfile.STATE_CONNECTED,
191                     BluetoothProfile.STATE_CONNECTING,
192                     BluetoothProfile.STATE_DISCONNECTING
193                 });
194     }
195 
getDevicesByStates(int[] states)196     private List<BluetoothDevice> getDevicesByStates(int[] states) {
197         if (mService == null) {
198             return new ArrayList<>(0);
199         }
200         return mService.getDevicesMatchingConnectionStates(states);
201     }
202 
203     /*
204      * @hide
205      */
connect(BluetoothDevice device)206     public boolean connect(BluetoothDevice device) {
207         if (mService == null) {
208             return false;
209         }
210         return mService.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
211     }
212 
213     /*
214      * @hide
215      */
disconnect(BluetoothDevice device)216     public boolean disconnect(BluetoothDevice device) {
217         if (mService == null) {
218             return false;
219         }
220         return mService.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
221     }
222 
getConnectionStatus(BluetoothDevice device)223     public int getConnectionStatus(BluetoothDevice device) {
224         if (mService == null) {
225             return BluetoothProfile.STATE_DISCONNECTED;
226         }
227         return mService.getConnectionState(device);
228     }
229 
230     /** Get group id for {@link BluetoothDevice}. */
getGroupId(@onNull BluetoothDevice device)231     public int getGroupId(@NonNull BluetoothDevice device) {
232         if (mService == null) {
233             return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
234         }
235         return mService.getGroupId(device);
236     }
237 
setActiveDevice(BluetoothDevice device)238     public boolean setActiveDevice(BluetoothDevice device) {
239         if (mBluetoothAdapter == null) {
240             return false;
241         }
242         return device == null
243                 ? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_ALL)
244                 : mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_ALL);
245     }
246 
getActiveDevices()247     public List<BluetoothDevice> getActiveDevices() {
248         if (mBluetoothAdapter == null) {
249             return new ArrayList<>();
250         }
251         return mBluetoothAdapter.getActiveDevices(BluetoothProfile.LE_AUDIO);
252     }
253 
254     /**
255      * Get Lead device for the group.
256      *
257      * <p>Lead device is the device that can be used as an active device in the system. Active
258      * devices points to the Audio Device for the Le Audio group. This method returns the Lead
259      * devices for the connected LE Audio group and this device should be used in the
260      * setActiveDevice() method by other parts of the system, which wants to set to active a
261      * particular Le Audio group.
262      *
263      * <p>Note: getActiveDevice() returns the Lead device for the currently active LE Audio group.
264      * Note: When Lead device gets disconnected while Le Audio group is active and has more devices
265      * in the group, then Lead device will not change. If Lead device gets disconnected, for the Le
266      * Audio group which is not active, a new Lead device will be chosen
267      *
268      * @param groupId The group id.
269      * @return group lead device.
270      * @hide
271      */
272     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
getConnectedGroupLeadDevice(int groupId)273     public @Nullable BluetoothDevice getConnectedGroupLeadDevice(int groupId) {
274         if (DEBUG) {
275             Log.d(TAG, "getConnectedGroupLeadDevice");
276         }
277         if (mService == null) {
278             Log.e(TAG, "No service.");
279             return null;
280         }
281         return mService.getConnectedGroupLeadDevice(groupId);
282     }
283 
284     @Override
isEnabled(BluetoothDevice device)285     public boolean isEnabled(BluetoothDevice device) {
286         if (mService == null || device == null) {
287             return false;
288         }
289         return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
290     }
291 
292     @Override
getConnectionPolicy(BluetoothDevice device)293     public int getConnectionPolicy(BluetoothDevice device) {
294         if (mService == null || device == null) {
295             return CONNECTION_POLICY_FORBIDDEN;
296         }
297         return mService.getConnectionPolicy(device);
298     }
299 
300     @Override
setEnabled(BluetoothDevice device, boolean enabled)301     public boolean setEnabled(BluetoothDevice device, boolean enabled) {
302         boolean isSuccessful = false;
303         if (mService == null || device == null) {
304             return false;
305         }
306         if (enabled) {
307             if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
308                 isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
309             }
310         } else {
311             isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
312         }
313 
314         return isSuccessful;
315     }
316 
toString()317     public String toString() {
318         return NAME;
319     }
320 
getOrdinal()321     public int getOrdinal() {
322         return ORDINAL;
323     }
324 
getNameResource(BluetoothDevice device)325     public int getNameResource(BluetoothDevice device) {
326         return R.string.bluetooth_profile_le_audio;
327     }
328 
getSummaryResourceForDevice(BluetoothDevice device)329     public int getSummaryResourceForDevice(BluetoothDevice device) {
330         int state = getConnectionStatus(device);
331         switch (state) {
332             case BluetoothProfile.STATE_DISCONNECTED:
333                 return R.string.bluetooth_le_audio_profile_summary_use_for;
334 
335             case BluetoothProfile.STATE_CONNECTED:
336                 return R.string.bluetooth_le_audio_profile_summary_connected;
337 
338             default:
339                 return BluetoothUtils.getConnectionStateSummary(state);
340         }
341     }
342 
getDrawableResource(BluetoothClass btClass)343     public int getDrawableResource(BluetoothClass btClass) {
344         if (btClass == null) {
345             Log.e(TAG, "No btClass.");
346             return R.drawable.ic_bt_le_audio_speakers;
347         }
348         switch (btClass.getDeviceClass()) {
349             case BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED:
350             case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
351             case BluetoothClass.Device.AUDIO_VIDEO_MICROPHONE:
352             case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES:
353                 return R.drawable.ic_bt_le_audio;
354             default:
355                 return R.drawable.ic_bt_le_audio_speakers;
356         }
357     }
358 
getAudioLocation(BluetoothDevice device)359     public int getAudioLocation(BluetoothDevice device) {
360         if (mService == null || device == null) {
361             return BluetoothLeAudio.AUDIO_LOCATION_INVALID;
362         }
363         return mService.getAudioLocation(device);
364     }
365 
366     /**
367      * Sets the fallback group id when broadcast switches to unicast.
368      *
369      * @param groupId the target fallback group id
370      */
setBroadcastToUnicastFallbackGroup(int groupId)371     public void setBroadcastToUnicastFallbackGroup(int groupId) {
372         if (mService == null) {
373             Log.w(TAG, "Proxy not attached to service. Cannot set fallback group: " + groupId);
374             return;
375         }
376 
377         mService.setBroadcastToUnicastFallbackGroup(groupId);
378     }
379 
380     /**
381      * Gets the fallback group id when broadcast switches to unicast.
382      *
383      * @return current fallback group id
384      */
getBroadcastToUnicastFallbackGroup()385     public int getBroadcastToUnicastFallbackGroup() {
386         if (mService == null) {
387             Log.w(TAG, "Proxy not attached to service. Cannot get fallback group.");
388             return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
389         }
390         return mService.getBroadcastToUnicastFallbackGroup();
391     }
392 
393     /**
394      * Registers a {@link BluetoothLeAudio.Callback} that will be invoked during the
395      * operation of this profile.
396      *
397      * Repeated registration of the same <var>callback</var> object after the first call to this
398      * method will result with IllegalArgumentException being thrown, even when the
399      * <var>executor</var> is different. API caller would have to call
400      * {@link #unregisterCallback(BluetoothLeAudio.Callback)} with the same callback object
401      * before registering it again.
402      *
403      * @param executor an {@link Executor} to execute given callback
404      * @param callback user implementation of the {@link BluetoothLeAudio.Callback}
405      * @throws NullPointerException if a null executor, or callback is given, or
406      *                              IllegalArgumentException if the same <var>callback</var> is
407      *                              already registered.
408      */
registerCallback( @onNull @allbackExecutor Executor executor, @NonNull BluetoothLeAudio.Callback callback)409     public void registerCallback(
410             @NonNull @CallbackExecutor Executor executor,
411             @NonNull BluetoothLeAudio.Callback callback) {
412         if (mService == null) {
413             Log.w(TAG, "Proxy not attached to service. Cannot register callback.");
414             if (Flags.adoptPrimaryGroupManagementApiV2()) {
415                 mCachedCallbackExecutorMap.putIfAbsent(callback, executor);
416             }
417             return;
418         }
419         mService.registerCallback(executor, callback);
420     }
421 
422     /**
423      * Unregisters the specified {@link BluetoothLeAudio.Callback}.
424      * <p>The same {@link BluetoothLeAudio.Callback} object used when calling
425      * {@link #registerCallback(Executor, BluetoothLeAudio.Callback)} must be used.
426      *
427      * <p>Callbacks are automatically unregistered when application process goes away
428      *
429      * @param callback user implementation of the {@link BluetoothLeAudio.Callback}
430      * @throws NullPointerException when callback is null or IllegalArgumentException when no
431      *                              callback is registered
432      */
unregisterCallback(@onNull BluetoothLeAudio.Callback callback)433     public void unregisterCallback(@NonNull BluetoothLeAudio.Callback callback) {
434         if (Flags.adoptPrimaryGroupManagementApiV2()) {
435             mCachedCallbackExecutorMap.remove(callback);
436         }
437         if (mService == null) {
438             Log.w(TAG, "Proxy not attached to service. Cannot unregister callback.");
439             return;
440         }
441         mService.unregisterCallback(callback);
442     }
443 
444     @RequiresApi(Build.VERSION_CODES.S)
finalize()445     protected void finalize() {
446         if (DEBUG) {
447             Log.d(TAG, "finalize()");
448         }
449         if (mService != null) {
450             try {
451                 BluetoothAdapter.getDefaultAdapter()
452                         .closeProfileProxy(BluetoothProfile.LE_AUDIO, mService);
453                 mService = null;
454             } catch (Throwable t) {
455                 Log.w(TAG, "Error cleaning up LeAudio proxy", t);
456             }
457         }
458     }
459 }
460