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