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