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