1 /* 2 * Copyright 2020 HIMSA II K/S - www.himsa.com. 3 * Represented by EHIMA - www.ehima.com 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 android.bluetooth; 19 20 import android.Manifest; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.RequiresPermission; 24 import android.annotation.SdkConstant; 25 import android.annotation.SdkConstant.SdkConstantType; 26 import android.annotation.SuppressLint; 27 import android.bluetooth.annotations.RequiresBluetoothConnectPermission; 28 import android.bluetooth.annotations.RequiresLegacyBluetoothPermission; 29 import android.content.Attributable; 30 import android.content.AttributionSource; 31 import android.content.Context; 32 import android.os.Binder; 33 import android.os.IBinder; 34 import android.os.RemoteException; 35 import android.util.CloseGuard; 36 import android.util.Log; 37 38 import java.util.ArrayList; 39 import java.util.List; 40 41 /** 42 * This class provides the public APIs to control the LeAudio profile. 43 * 44 * <p>BluetoothLeAudio is a proxy object for controlling the Bluetooth LE Audio 45 * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get 46 * the BluetoothLeAudio proxy object. 47 * 48 * <p> Android only supports one set of connected Bluetooth LeAudio device at a time. Each 49 * method is protected with its appropriate permission. 50 */ 51 public final class BluetoothLeAudio implements BluetoothProfile, AutoCloseable { 52 private static final String TAG = "BluetoothLeAudio"; 53 private static final boolean DBG = false; 54 private static final boolean VDBG = false; 55 56 private CloseGuard mCloseGuard; 57 58 /** 59 * Intent used to broadcast the change in connection state of the LeAudio 60 * profile. Please note that in the binaural case, there will be two different LE devices for 61 * the left and right side and each device will have their own connection state changes. 62 * 63 * <p>This intent will have 3 extras: 64 * <ul> 65 * <li> {@link #EXTRA_STATE} - The current state of the profile. </li> 66 * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li> 67 * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> 68 * </ul> 69 * 70 * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of 71 * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, 72 * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}. 73 */ 74 @RequiresLegacyBluetoothPermission 75 @RequiresBluetoothConnectPermission 76 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 77 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 78 public static final String ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED = 79 "android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED"; 80 81 /** 82 * Intent used to broadcast the selection of a connected device as active. 83 * 84 * <p>This intent will have one extra: 85 * <ul> 86 * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can 87 * be null if no device is active. </li> 88 * </ul> 89 * 90 * @hide 91 */ 92 @RequiresLegacyBluetoothPermission 93 @RequiresBluetoothConnectPermission 94 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 95 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 96 public static final String ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED = 97 "android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED"; 98 99 /** 100 * This represents an invalid group ID. 101 * 102 * @hide 103 */ 104 public static final int GROUP_ID_INVALID = IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID; 105 106 private final BluetoothAdapter mAdapter; 107 private final AttributionSource mAttributionSource; 108 private final BluetoothProfileConnector<IBluetoothLeAudio> mProfileConnector = 109 new BluetoothProfileConnector(this, BluetoothProfile.LE_AUDIO, "BluetoothLeAudio", 110 IBluetoothLeAudio.class.getName()) { 111 @Override 112 public IBluetoothLeAudio getServiceInterface(IBinder service) { 113 return IBluetoothLeAudio.Stub.asInterface(Binder.allowBlocking(service)); 114 } 115 }; 116 117 /** 118 * Create a BluetoothLeAudio proxy object for interacting with the local 119 * Bluetooth LeAudio service. 120 */ BluetoothLeAudio(Context context, ServiceListener listener, BluetoothAdapter adapter)121 /* package */ BluetoothLeAudio(Context context, ServiceListener listener, 122 BluetoothAdapter adapter) { 123 mAdapter = adapter; 124 mAttributionSource = adapter.getAttributionSource(); 125 mProfileConnector.connect(context, listener); 126 mCloseGuard = new CloseGuard(); 127 mCloseGuard.open("close"); 128 } 129 130 /** 131 * @hide 132 */ close()133 public void close() { 134 mProfileConnector.disconnect(); 135 } 136 getService()137 private IBluetoothLeAudio getService() { 138 return mProfileConnector.getService(); 139 } 140 finalize()141 protected void finalize() { 142 if (mCloseGuard != null) { 143 mCloseGuard.warnIfOpen(); 144 } 145 close(); 146 } 147 148 /** 149 * Initiate connection to a profile of the remote bluetooth device. 150 * 151 * <p> This API returns false in scenarios like the profile on the 152 * device is already connected or Bluetooth is not turned on. 153 * When this API returns true, it is guaranteed that 154 * connection state intent for the profile will be broadcasted with 155 * the state. Users can get the connection state of the profile 156 * from this intent. 157 * 158 * 159 * @param device Remote Bluetooth Device 160 * @return false on immediate error, true otherwise 161 * @hide 162 */ 163 @RequiresBluetoothConnectPermission 164 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) connect(@ullable BluetoothDevice device)165 public boolean connect(@Nullable BluetoothDevice device) { 166 if (DBG) log("connect(" + device + ")"); 167 try { 168 final IBluetoothLeAudio service = getService(); 169 if (service != null && mAdapter.isEnabled() && isValidDevice(device)) { 170 return service.connect(device, mAttributionSource); 171 } 172 if (service == null) Log.w(TAG, "Proxy not attached to service"); 173 return false; 174 } catch (RemoteException e) { 175 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 176 return false; 177 } 178 } 179 180 /** 181 * Initiate disconnection from a profile 182 * 183 * <p> This API will return false in scenarios like the profile on the 184 * Bluetooth device is not in connected state etc. When this API returns, 185 * true, it is guaranteed that the connection state change 186 * intent will be broadcasted with the state. Users can get the 187 * disconnection state of the profile from this intent. 188 * 189 * <p> If the disconnection is initiated by a remote device, the state 190 * will transition from {@link #STATE_CONNECTED} to 191 * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the 192 * host (local) device the state will transition from 193 * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to 194 * state {@link #STATE_DISCONNECTED}. The transition to 195 * {@link #STATE_DISCONNECTING} can be used to distinguish between the 196 * two scenarios. 197 * 198 * 199 * @param device Remote Bluetooth Device 200 * @return false on immediate error, true otherwise 201 * @hide 202 */ 203 @RequiresBluetoothConnectPermission 204 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) disconnect(@ullable BluetoothDevice device)205 public boolean disconnect(@Nullable BluetoothDevice device) { 206 if (DBG) log("disconnect(" + device + ")"); 207 try { 208 final IBluetoothLeAudio service = getService(); 209 if (service != null && mAdapter.isEnabled() && isValidDevice(device)) { 210 return service.disconnect(device, mAttributionSource); 211 } 212 if (service == null) Log.w(TAG, "Proxy not attached to service"); 213 return false; 214 } catch (RemoteException e) { 215 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 216 return false; 217 } 218 } 219 220 /** 221 * {@inheritDoc} 222 */ 223 @Override 224 @RequiresBluetoothConnectPermission 225 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getConnectedDevices()226 public @NonNull List<BluetoothDevice> getConnectedDevices() { 227 if (VDBG) log("getConnectedDevices()"); 228 try { 229 final IBluetoothLeAudio service = getService(); 230 if (service != null && mAdapter.isEnabled()) { 231 return Attributable.setAttributionSource( 232 service.getConnectedDevices(mAttributionSource), mAttributionSource); 233 } 234 if (service == null) Log.w(TAG, "Proxy not attached to service"); 235 return new ArrayList<BluetoothDevice>(); 236 } catch (RemoteException e) { 237 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 238 return new ArrayList<BluetoothDevice>(); 239 } 240 } 241 242 /** 243 * {@inheritDoc} 244 */ 245 @Override 246 @RequiresBluetoothConnectPermission 247 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getDevicesMatchingConnectionStates( @onNull int[] states)248 public @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates( 249 @NonNull int[] states) { 250 if (VDBG) log("getDevicesMatchingStates()"); 251 try { 252 final IBluetoothLeAudio service = getService(); 253 if (service != null && mAdapter.isEnabled()) { 254 return Attributable.setAttributionSource( 255 service.getDevicesMatchingConnectionStates(states, mAttributionSource), 256 mAttributionSource); 257 } 258 if (service == null) Log.w(TAG, "Proxy not attached to service"); 259 return new ArrayList<BluetoothDevice>(); 260 } catch (RemoteException e) { 261 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 262 return new ArrayList<BluetoothDevice>(); 263 } 264 } 265 266 /** 267 * {@inheritDoc} 268 */ 269 @Override 270 @RequiresLegacyBluetoothPermission 271 @RequiresBluetoothConnectPermission 272 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getConnectionState(@onNull BluetoothDevice device)273 public @BtProfileState int getConnectionState(@NonNull BluetoothDevice device) { 274 if (VDBG) log("getState(" + device + ")"); 275 try { 276 final IBluetoothLeAudio service = getService(); 277 if (service != null && mAdapter.isEnabled() 278 && isValidDevice(device)) { 279 return service.getConnectionState(device, mAttributionSource); 280 } 281 if (service == null) Log.w(TAG, "Proxy not attached to service"); 282 return BluetoothProfile.STATE_DISCONNECTED; 283 } catch (RemoteException e) { 284 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 285 return BluetoothProfile.STATE_DISCONNECTED; 286 } 287 } 288 289 /** 290 * Select a connected device as active. 291 * 292 * The active device selection is per profile. An active device's 293 * purpose is profile-specific. For example, LeAudio audio 294 * streaming is to the active LeAudio device. If a remote device 295 * is not connected, it cannot be selected as active. 296 * 297 * <p> This API returns false in scenarios like the profile on the 298 * device is not connected or Bluetooth is not turned on. 299 * When this API returns true, it is guaranteed that the 300 * {@link #ACTION_LEAUDIO_ACTIVE_DEVICE_CHANGED} intent will be broadcasted 301 * with the active device. 302 * 303 * 304 * @param device the remote Bluetooth device. Could be null to clear 305 * the active device and stop streaming audio to a Bluetooth device. 306 * @return false on immediate error, true otherwise 307 * @hide 308 */ 309 @RequiresBluetoothConnectPermission 310 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) setActiveDevice(@ullable BluetoothDevice device)311 public boolean setActiveDevice(@Nullable BluetoothDevice device) { 312 if (DBG) log("setActiveDevice(" + device + ")"); 313 try { 314 final IBluetoothLeAudio service = getService(); 315 if (service != null && mAdapter.isEnabled() 316 && ((device == null) || isValidDevice(device))) { 317 service.setActiveDevice(device, mAttributionSource); 318 return true; 319 } 320 if (service == null) Log.w(TAG, "Proxy not attached to service"); 321 return false; 322 } catch (RemoteException e) { 323 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 324 return false; 325 } 326 } 327 328 /** 329 * Get the connected LeAudio devices that are active 330 * 331 * @return the list of active devices. Returns empty list on error. 332 * @hide 333 */ 334 @NonNull 335 @RequiresLegacyBluetoothPermission 336 @RequiresBluetoothConnectPermission 337 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getActiveDevices()338 public List<BluetoothDevice> getActiveDevices() { 339 if (VDBG) log("getActiveDevices()"); 340 try { 341 final IBluetoothLeAudio service = getService(); 342 if (service != null && mAdapter.isEnabled()) { 343 return Attributable.setAttributionSource( 344 service.getActiveDevices(mAttributionSource), mAttributionSource); 345 } 346 if (service == null) Log.w(TAG, "Proxy not attached to service"); 347 return new ArrayList<>(); 348 } catch (RemoteException e) { 349 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 350 return new ArrayList<>(); 351 } 352 } 353 354 /** 355 * Get device group id. Devices with same group id belong to same group (i.e left and right 356 * earbud) 357 * @param device LE Audio capable device 358 * @return group id that this device currently belongs to 359 * @hide 360 */ 361 @RequiresLegacyBluetoothPermission 362 @RequiresBluetoothConnectPermission 363 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getGroupId(@onNull BluetoothDevice device)364 public int getGroupId(@NonNull BluetoothDevice device) { 365 if (VDBG) log("getGroupId()"); 366 try { 367 final IBluetoothLeAudio service = getService(); 368 if (service != null && mAdapter.isEnabled()) { 369 return service.getGroupId(device, mAttributionSource); 370 } 371 if (service == null) Log.w(TAG, "Proxy not attached to service"); 372 return GROUP_ID_INVALID; 373 } catch (RemoteException e) { 374 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 375 return GROUP_ID_INVALID; 376 } 377 } 378 379 /** 380 * Set connection policy of the profile 381 * 382 * <p> The device should already be paired. 383 * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED}, 384 * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN} 385 * 386 * @param device Paired bluetooth device 387 * @param connectionPolicy is the connection policy to set to for this profile 388 * @return true if connectionPolicy is set, false on error 389 * @hide 390 */ 391 @RequiresBluetoothConnectPermission 392 @RequiresPermission(allOf = { 393 android.Manifest.permission.BLUETOOTH_CONNECT, 394 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 395 }) setConnectionPolicy(@onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)396 public boolean setConnectionPolicy(@NonNull BluetoothDevice device, 397 @ConnectionPolicy int connectionPolicy) { 398 if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")"); 399 try { 400 final IBluetoothLeAudio service = getService(); 401 if (service != null && mAdapter.isEnabled() 402 && isValidDevice(device)) { 403 if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_FORBIDDEN 404 && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) { 405 return false; 406 } 407 return service.setConnectionPolicy(device, connectionPolicy, mAttributionSource); 408 } 409 if (service == null) Log.w(TAG, "Proxy not attached to service"); 410 return false; 411 } catch (RemoteException e) { 412 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 413 return false; 414 } 415 } 416 417 /** 418 * Get the connection policy of the profile. 419 * 420 * <p> The connection policy can be any of: 421 * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, 422 * {@link #CONNECTION_POLICY_UNKNOWN} 423 * 424 * @param device Bluetooth device 425 * @return connection policy of the device 426 * @hide 427 */ 428 @RequiresBluetoothConnectPermission 429 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getConnectionPolicy(@ullable BluetoothDevice device)430 public @ConnectionPolicy int getConnectionPolicy(@Nullable BluetoothDevice device) { 431 if (VDBG) log("getConnectionPolicy(" + device + ")"); 432 try { 433 final IBluetoothLeAudio service = getService(); 434 if (service != null && mAdapter.isEnabled() 435 && isValidDevice(device)) { 436 return service.getConnectionPolicy(device, mAttributionSource); 437 } 438 if (service == null) Log.w(TAG, "Proxy not attached to service"); 439 return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 440 } catch (RemoteException e) { 441 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 442 return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 443 } 444 } 445 446 447 /** 448 * Helper for converting a state to a string. 449 * 450 * For debug use only - strings are not internationalized. 451 * 452 * @hide 453 */ stateToString(int state)454 public static String stateToString(int state) { 455 switch (state) { 456 case STATE_DISCONNECTED: 457 return "disconnected"; 458 case STATE_CONNECTING: 459 return "connecting"; 460 case STATE_CONNECTED: 461 return "connected"; 462 case STATE_DISCONNECTING: 463 return "disconnecting"; 464 default: 465 return "<unknown state " + state + ">"; 466 } 467 } 468 isValidDevice(@ullable BluetoothDevice device)469 private boolean isValidDevice(@Nullable BluetoothDevice device) { 470 if (device == null) return false; 471 472 if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true; 473 return false; 474 } 475 log(String msg)476 private static void log(String msg) { 477 Log.d(TAG, msg); 478 } 479 } 480