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.bluetooth.BluetoothAdapter;
25 import android.bluetooth.BluetoothClass;
26 import android.bluetooth.BluetoothCsipSetCoordinator;
27 import android.bluetooth.BluetoothDevice;
28 import android.bluetooth.BluetoothLeAudio;
29 import android.bluetooth.BluetoothProfile;
30 import android.content.Context;
31 import android.os.Build;
32 import android.util.Log;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.annotation.RequiresApi;
37 
38 import com.android.settingslib.R;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 public class LeAudioProfile implements LocalBluetoothProfile {
44     private static final String TAG = "LeAudioProfile";
45     private static boolean DEBUG = true;
46 
47     private Context mContext;
48 
49     private BluetoothLeAudio mService;
50     private boolean mIsProfileReady;
51 
52     private final CachedBluetoothDeviceManager mDeviceManager;
53 
54     static final String NAME = "LE_AUDIO";
55     private final LocalBluetoothProfileManager mProfileManager;
56     private final BluetoothAdapter mBluetoothAdapter;
57 
58     // Order of this profile in device profiles list
59     private static final int ORDINAL = 1;
60 
61     // These callbacks run on the main thread.
62     private final class LeAudioServiceListener implements BluetoothProfile.ServiceListener {
63 
64         @RequiresApi(Build.VERSION_CODES.S)
onServiceConnected(int profile, BluetoothProfile proxy)65         public void onServiceConnected(int profile, BluetoothProfile proxy) {
66             if (DEBUG) {
67                 Log.d(TAG, "Bluetooth service connected");
68             }
69             mService = (BluetoothLeAudio) proxy;
70             // We just bound to the service, so refresh the UI for any connected LeAudio devices.
71             List<BluetoothDevice> deviceList = mService.getConnectedDevices();
72             while (!deviceList.isEmpty()) {
73                 BluetoothDevice nextDevice = deviceList.remove(0);
74                 CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
75                 // we may add a new device here, but generally this should not happen
76                 if (device == null) {
77                     if (DEBUG) {
78                         Log.d(TAG, "LeAudioProfile found new device: " + nextDevice);
79                     }
80                     device = mDeviceManager.addDevice(nextDevice);
81                 }
82                 device.onProfileStateChanged(LeAudioProfile.this, BluetoothProfile.STATE_CONNECTED);
83                 device.refresh();
84             }
85 
86             // Check current list of CachedDevices to see if any are hearing aid devices.
87             mDeviceManager.updateHearingAidsDevices();
88             mProfileManager.callServiceConnectedListeners();
89             mIsProfileReady = true;
90         }
91 
onServiceDisconnected(int profile)92         public void onServiceDisconnected(int profile) {
93             if (DEBUG) {
94                 Log.d(TAG, "Bluetooth service disconnected");
95             }
96             mProfileManager.callServiceDisconnectedListeners();
97             mIsProfileReady = false;
98         }
99     }
100 
isProfileReady()101     public boolean isProfileReady() {
102         return mIsProfileReady;
103     }
104 
105     @Override
getProfileId()106     public int getProfileId() {
107         return BluetoothProfile.LE_AUDIO;
108     }
109 
LeAudioProfile( Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager)110     LeAudioProfile(
111             Context context,
112             CachedBluetoothDeviceManager deviceManager,
113             LocalBluetoothProfileManager profileManager) {
114         mContext = context;
115         mDeviceManager = deviceManager;
116         mProfileManager = profileManager;
117 
118         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
119         mBluetoothAdapter.getProfileProxy(
120                 context, new LeAudioServiceListener(), BluetoothProfile.LE_AUDIO);
121     }
122 
accessProfileEnabled()123     public boolean accessProfileEnabled() {
124         return true;
125     }
126 
isAutoConnectable()127     public boolean isAutoConnectable() {
128         return true;
129     }
130 
getConnectedDevices()131     public List<BluetoothDevice> getConnectedDevices() {
132         return getDevicesByStates(
133                 new int[] {
134                     BluetoothProfile.STATE_CONNECTED,
135                     BluetoothProfile.STATE_CONNECTING,
136                     BluetoothProfile.STATE_DISCONNECTING
137                 });
138     }
139 
getConnectableDevices()140     public List<BluetoothDevice> getConnectableDevices() {
141         return getDevicesByStates(
142                 new int[] {
143                     BluetoothProfile.STATE_DISCONNECTED,
144                     BluetoothProfile.STATE_CONNECTED,
145                     BluetoothProfile.STATE_CONNECTING,
146                     BluetoothProfile.STATE_DISCONNECTING
147                 });
148     }
149 
getDevicesByStates(int[] states)150     private List<BluetoothDevice> getDevicesByStates(int[] states) {
151         if (mService == null) {
152             return new ArrayList<>(0);
153         }
154         return mService.getDevicesMatchingConnectionStates(states);
155     }
156 
157     /*
158      * @hide
159      */
connect(BluetoothDevice device)160     public boolean connect(BluetoothDevice device) {
161         if (mService == null) {
162             return false;
163         }
164         return mService.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
165     }
166 
167     /*
168      * @hide
169      */
disconnect(BluetoothDevice device)170     public boolean disconnect(BluetoothDevice device) {
171         if (mService == null) {
172             return false;
173         }
174         return mService.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
175     }
176 
getConnectionStatus(BluetoothDevice device)177     public int getConnectionStatus(BluetoothDevice device) {
178         if (mService == null) {
179             return BluetoothProfile.STATE_DISCONNECTED;
180         }
181         return mService.getConnectionState(device);
182     }
183 
184     /** Get group id for {@link BluetoothDevice}. */
getGroupId(@onNull BluetoothDevice device)185     public int getGroupId(@NonNull BluetoothDevice device) {
186         if (mService == null) {
187             return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
188         }
189         return mService.getGroupId(device);
190     }
191 
setActiveDevice(BluetoothDevice device)192     public boolean setActiveDevice(BluetoothDevice device) {
193         if (mBluetoothAdapter == null) {
194             return false;
195         }
196         return device == null
197                 ? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_ALL)
198                 : mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_ALL);
199     }
200 
getActiveDevices()201     public List<BluetoothDevice> getActiveDevices() {
202         if (mBluetoothAdapter == null) {
203             return new ArrayList<>();
204         }
205         return mBluetoothAdapter.getActiveDevices(BluetoothProfile.LE_AUDIO);
206     }
207 
208     /**
209      * Get Lead device for the group.
210      *
211      * <p>Lead device is the device that can be used as an active device in the system. Active
212      * devices points to the Audio Device for the Le Audio group. This method returns the Lead
213      * devices for the connected LE Audio group and this device should be used in the
214      * setActiveDevice() method by other parts of the system, which wants to set to active a
215      * particular Le Audio group.
216      *
217      * <p>Note: getActiveDevice() returns the Lead device for the currently active LE Audio group.
218      * Note: When Lead device gets disconnected while Le Audio group is active and has more devices
219      * in the group, then Lead device will not change. If Lead device gets disconnected, for the Le
220      * Audio group which is not active, a new Lead device will be chosen
221      *
222      * @param groupId The group id.
223      * @return group lead device.
224      * @hide
225      */
226     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
getConnectedGroupLeadDevice(int groupId)227     public @Nullable BluetoothDevice getConnectedGroupLeadDevice(int groupId) {
228         if (DEBUG) {
229             Log.d(TAG, "getConnectedGroupLeadDevice");
230         }
231         if (mService == null) {
232             Log.e(TAG, "No service.");
233             return null;
234         }
235         return mService.getConnectedGroupLeadDevice(groupId);
236     }
237 
238     @Override
isEnabled(BluetoothDevice device)239     public boolean isEnabled(BluetoothDevice device) {
240         if (mService == null || device == null) {
241             return false;
242         }
243         return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
244     }
245 
246     @Override
getConnectionPolicy(BluetoothDevice device)247     public int getConnectionPolicy(BluetoothDevice device) {
248         if (mService == null || device == null) {
249             return CONNECTION_POLICY_FORBIDDEN;
250         }
251         return mService.getConnectionPolicy(device);
252     }
253 
254     @Override
setEnabled(BluetoothDevice device, boolean enabled)255     public boolean setEnabled(BluetoothDevice device, boolean enabled) {
256         boolean isSuccessful = false;
257         if (mService == null || device == null) {
258             return false;
259         }
260         if (enabled) {
261             if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
262                 isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
263             }
264         } else {
265             isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
266         }
267 
268         return isSuccessful;
269     }
270 
toString()271     public String toString() {
272         return NAME;
273     }
274 
getOrdinal()275     public int getOrdinal() {
276         return ORDINAL;
277     }
278 
getNameResource(BluetoothDevice device)279     public int getNameResource(BluetoothDevice device) {
280         return R.string.bluetooth_profile_le_audio;
281     }
282 
getSummaryResourceForDevice(BluetoothDevice device)283     public int getSummaryResourceForDevice(BluetoothDevice device) {
284         int state = getConnectionStatus(device);
285         switch (state) {
286             case BluetoothProfile.STATE_DISCONNECTED:
287                 return R.string.bluetooth_le_audio_profile_summary_use_for;
288 
289             case BluetoothProfile.STATE_CONNECTED:
290                 return R.string.bluetooth_le_audio_profile_summary_connected;
291 
292             default:
293                 return BluetoothUtils.getConnectionStateSummary(state);
294         }
295     }
296 
getDrawableResource(BluetoothClass btClass)297     public int getDrawableResource(BluetoothClass btClass) {
298         if (btClass == null) {
299             Log.e(TAG, "No btClass.");
300             return R.drawable.ic_bt_le_audio_speakers;
301         }
302         switch (btClass.getDeviceClass()) {
303             case BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED:
304             case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
305             case BluetoothClass.Device.AUDIO_VIDEO_MICROPHONE:
306             case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES:
307                 return R.drawable.ic_bt_le_audio;
308             default:
309                 return R.drawable.ic_bt_le_audio_speakers;
310         }
311     }
312 
getAudioLocation(BluetoothDevice device)313     public int getAudioLocation(BluetoothDevice device) {
314         if (mService == null || device == null) {
315             return BluetoothLeAudio.AUDIO_LOCATION_INVALID;
316         }
317         return mService.getAudioLocation(device);
318     }
319 
320     @RequiresApi(Build.VERSION_CODES.S)
finalize()321     protected void finalize() {
322         if (DEBUG) {
323             Log.d(TAG, "finalize()");
324         }
325         if (mService != null) {
326             try {
327                 BluetoothAdapter.getDefaultAdapter()
328                         .closeProfileProxy(BluetoothProfile.LE_AUDIO, mService);
329                 mService = null;
330             } catch (Throwable t) {
331                 Log.w(TAG, "Error cleaning up LeAudio proxy", t);
332             }
333         }
334     }
335 }
336