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