1 /* 2 * Copyright 2021 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 static android.bluetooth.BluetoothUtils.getSyncTimeout; 21 22 import android.Manifest; 23 import android.annotation.CallbackExecutor; 24 import android.annotation.IntDef; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.annotation.RequiresPermission; 28 import android.annotation.SdkConstant; 29 import android.annotation.SdkConstant.SdkConstantType; 30 import android.annotation.SystemApi; 31 import android.content.AttributionSource; 32 import android.content.Context; 33 import android.os.IBinder; 34 import android.os.ParcelUuid; 35 import android.os.RemoteException; 36 import android.util.CloseGuard; 37 import android.util.Log; 38 39 import com.android.modules.utils.SynchronousResultReceiver; 40 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.HashMap; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Objects; 49 import java.util.UUID; 50 import java.util.concurrent.Executor; 51 import java.util.concurrent.TimeoutException; 52 53 /** 54 * This class provides the public APIs to control the Bluetooth CSIP set coordinator. 55 * 56 * <p>BluetoothCsipSetCoordinator is a proxy object for controlling the Bluetooth CSIP set 57 * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get 58 * the BluetoothCsipSetCoordinator proxy object. 59 * 60 */ 61 public final class BluetoothCsipSetCoordinator implements BluetoothProfile, AutoCloseable { 62 private static final String TAG = "BluetoothCsipSetCoordinator"; 63 private static final boolean DBG = false; 64 private static final boolean VDBG = false; 65 66 private CloseGuard mCloseGuard; 67 68 /** 69 * @hide 70 */ 71 @SystemApi 72 public interface ClientLockCallback { 73 /** @hide */ 74 @IntDef(value = { 75 BluetoothStatusCodes.SUCCESS, 76 BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED, 77 BluetoothStatusCodes.ERROR_CSIP_INVALID_GROUP_ID, 78 BluetoothStatusCodes.ERROR_CSIP_GROUP_LOCKED_BY_OTHER, 79 BluetoothStatusCodes.ERROR_CSIP_LOCKED_GROUP_MEMBER_LOST, 80 BluetoothStatusCodes.ERROR_UNKNOWN, 81 }) 82 @Retention(RetentionPolicy.SOURCE) 83 @interface Status {} 84 85 /** 86 * Callback is invoken as a result on {@link #groupLock()}. 87 * 88 * @param groupId group identifier 89 * @param opStatus status of lock operation 90 * @param isLocked inidcates if group is locked 91 * 92 * @hide 93 */ 94 @SystemApi onGroupLockSet(int groupId, @Status int opStatus, boolean isLocked)95 void onGroupLockSet(int groupId, @Status int opStatus, boolean isLocked); 96 } 97 98 private static class BluetoothCsipSetCoordinatorLockCallbackDelegate 99 extends IBluetoothCsipSetCoordinatorLockCallback.Stub { 100 private final ClientLockCallback mCallback; 101 private final Executor mExecutor; 102 BluetoothCsipSetCoordinatorLockCallbackDelegate( Executor executor, ClientLockCallback callback)103 BluetoothCsipSetCoordinatorLockCallbackDelegate( 104 Executor executor, ClientLockCallback callback) { 105 mExecutor = executor; 106 mCallback = callback; 107 } 108 109 @Override onGroupLockSet(int groupId, int opStatus, boolean isLocked)110 public void onGroupLockSet(int groupId, int opStatus, boolean isLocked) { 111 mExecutor.execute(() -> mCallback.onGroupLockSet(groupId, opStatus, isLocked)); 112 } 113 }; 114 115 /** 116 * Intent used to broadcast the change in connection state of the CSIS 117 * Client. 118 * 119 * <p>This intent will have 3 extras: 120 * <ul> 121 * <li> {@link #EXTRA_STATE} - The current state of the profile. </li> 122 * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li> 123 * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> 124 * </ul> 125 * 126 * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of 127 * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, 128 * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}. 129 */ 130 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 131 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 132 public static final String ACTION_CSIS_CONNECTION_STATE_CHANGED = 133 "android.bluetooth.action.CSIS_CONNECTION_STATE_CHANGED"; 134 135 /** 136 * Intent used to expose broadcast receiving device. 137 * 138 * <p>This intent will have 2 extras: 139 * <ul> 140 * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote Broadcast receiver device. </li> 141 * <li> {@link #EXTRA_CSIS_GROUP_ID} - Group identifier. </li> 142 * <li> {@link #EXTRA_CSIS_GROUP_SIZE} - Group size. </li> 143 * <li> {@link #EXTRA_CSIS_GROUP_TYPE_UUID} - Group type UUID. </li> 144 * </ul> 145 * 146 * @hide 147 */ 148 @SystemApi 149 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) 150 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 151 public static final String ACTION_CSIS_DEVICE_AVAILABLE = 152 "android.bluetooth.action.CSIS_DEVICE_AVAILABLE"; 153 154 /** 155 * Used as an extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent. 156 * Contains the group id. 157 * 158 * <p>Possible Values: 159 * {@link GROUP_ID_INVALID} Invalid group identifier 160 * 0x01 - 0xEF Valid group identifier 161 * @hide 162 */ 163 @SystemApi 164 public static final String EXTRA_CSIS_GROUP_ID = "android.bluetooth.extra.CSIS_GROUP_ID"; 165 166 /** 167 * Group size as int extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent. 168 * 169 * @hide 170 */ 171 public static final String EXTRA_CSIS_GROUP_SIZE = "android.bluetooth.extra.CSIS_GROUP_SIZE"; 172 173 /** 174 * Group type uuid extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent. 175 * 176 * @hide 177 */ 178 public static final String EXTRA_CSIS_GROUP_TYPE_UUID = 179 "android.bluetooth.extra.CSIS_GROUP_TYPE_UUID"; 180 181 /** 182 * Intent used to broadcast information about identified set member 183 * ready to connect. 184 * 185 * <p>This intent will have one extra: 186 * <ul> 187 * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can 188 * be null if no device is active. </li> 189 * <li> {@link #EXTRA_CSIS_GROUP_ID} - Group identifier. </li> 190 * </ul> 191 * 192 * @hide 193 */ 194 @SystemApi 195 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) 196 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 197 public static final String ACTION_CSIS_SET_MEMBER_AVAILABLE = 198 "android.bluetooth.action.CSIS_SET_MEMBER_AVAILABLE"; 199 200 /** 201 * This represents an invalid group ID. 202 * 203 * @hide 204 */ 205 @SystemApi 206 public static final int GROUP_ID_INVALID = IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID; 207 208 private final BluetoothAdapter mAdapter; 209 private final AttributionSource mAttributionSource; 210 private final BluetoothProfileConnector<IBluetoothCsipSetCoordinator> mProfileConnector = 211 new BluetoothProfileConnector(this, BluetoothProfile.CSIP_SET_COORDINATOR, TAG, 212 IBluetoothCsipSetCoordinator.class.getName()) { 213 @Override 214 public IBluetoothCsipSetCoordinator getServiceInterface(IBinder service) { 215 return IBluetoothCsipSetCoordinator.Stub.asInterface(service); 216 } 217 }; 218 219 /** 220 * Create a BluetoothCsipSetCoordinator proxy object for interacting with the local 221 * Bluetooth CSIS service. 222 */ BluetoothCsipSetCoordinator(Context context, ServiceListener listener, BluetoothAdapter adapter)223 /*package*/ BluetoothCsipSetCoordinator(Context context, ServiceListener listener, BluetoothAdapter adapter) { 224 mAdapter = adapter; 225 mAttributionSource = adapter.getAttributionSource(); 226 mProfileConnector.connect(context, listener); 227 mCloseGuard = new CloseGuard(); 228 mCloseGuard.open("close"); 229 } 230 231 /** 232 * @hide 233 */ finalize()234 protected void finalize() { 235 if (mCloseGuard != null) { 236 mCloseGuard.warnIfOpen(); 237 } 238 close(); 239 } 240 241 /** 242 * @hide 243 */ close()244 public void close() { 245 mProfileConnector.disconnect(); 246 } 247 getService()248 private IBluetoothCsipSetCoordinator getService() { 249 return mProfileConnector.getService(); 250 } 251 252 /** 253 * Lock the set. 254 * @param groupId group ID to lock, 255 * @param executor callback executor, 256 * @param callback callback to report lock and unlock events - stays valid until the app unlocks 257 * using the returned lock identifier or the lock timeouts on the remote side, 258 * as per CSIS specification, 259 * @return unique lock identifier used for unlocking or null if lock has failed. 260 * @throws {@link IllegalArgumentException} when executor or callback is null 261 * 262 * @hide 263 */ 264 @SystemApi 265 @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) 266 public lockGroup(int groupId, @NonNull @CallbackExecutor Executor executor, @NonNull ClientLockCallback callback)267 @Nullable UUID lockGroup(int groupId, @NonNull @CallbackExecutor Executor executor, 268 @NonNull ClientLockCallback callback) { 269 if (VDBG) log("lockGroup()"); 270 Objects.requireNonNull(executor, "executor cannot be null"); 271 Objects.requireNonNull(callback, "callback cannot be null"); 272 final IBluetoothCsipSetCoordinator service = getService(); 273 final UUID defaultValue = null; 274 if (service == null) { 275 Log.w(TAG, "Proxy not attached to service"); 276 if (DBG) log(Log.getStackTraceString(new Throwable())); 277 } else if (isEnabled()) { 278 IBluetoothCsipSetCoordinatorLockCallback delegate = 279 new BluetoothCsipSetCoordinatorLockCallbackDelegate(executor, callback); 280 try { 281 final SynchronousResultReceiver<ParcelUuid> recv = SynchronousResultReceiver.get(); 282 service.lockGroup(groupId, delegate, mAttributionSource, recv); 283 final ParcelUuid ret = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); 284 return ret == null ? defaultValue : ret.getUuid(); 285 } catch (TimeoutException e) { 286 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 287 } catch (RemoteException e) { 288 e.rethrowFromSystemServer(); 289 } 290 } 291 return defaultValue; 292 } 293 294 /** 295 * Unlock the set. 296 * @param lockUuid unique lock identifier 297 * @return true if unlocked, false on error 298 * @throws {@link IllegalArgumentException} when lockUuid is null 299 * 300 * @hide 301 */ 302 @SystemApi 303 @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) unlockGroup(@onNull UUID lockUuid)304 public boolean unlockGroup(@NonNull UUID lockUuid) { 305 if (VDBG) log("unlockGroup()"); 306 Objects.requireNonNull(lockUuid, "lockUuid cannot be null"); 307 final IBluetoothCsipSetCoordinator service = getService(); 308 final boolean defaultValue = false; 309 if (service == null) { 310 Log.w(TAG, "Proxy not attached to service"); 311 if (DBG) log(Log.getStackTraceString(new Throwable())); 312 } else if (isEnabled()) { 313 try { 314 final SynchronousResultReceiver recv = SynchronousResultReceiver.get(); 315 service.unlockGroup(new ParcelUuid(lockUuid), mAttributionSource, recv); 316 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); 317 return true; 318 } catch (TimeoutException e) { 319 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 320 } catch (RemoteException e) { 321 e.rethrowFromSystemServer(); 322 } 323 } 324 return defaultValue; 325 } 326 327 /** 328 * Get device's groups. 329 * @param device the active device 330 * @return Map of groups ids and related UUIDs 331 * 332 * @hide 333 */ 334 @SystemApi 335 @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) getGroupUuidMapByDevice( @ullable BluetoothDevice device)336 public @NonNull Map<Integer, ParcelUuid> getGroupUuidMapByDevice( 337 @Nullable BluetoothDevice device) { 338 if (VDBG) log("getGroupUuidMapByDevice()"); 339 final IBluetoothCsipSetCoordinator service = getService(); 340 final Map defaultValue = new HashMap<>(); 341 if (service == null) { 342 Log.w(TAG, "Proxy not attached to service"); 343 if (DBG) log(Log.getStackTraceString(new Throwable())); 344 } else if (isEnabled()) { 345 try { 346 final SynchronousResultReceiver<Map> recv = SynchronousResultReceiver.get(); 347 service.getGroupUuidMapByDevice(device, mAttributionSource, recv); 348 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 349 } catch (TimeoutException e) { 350 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 351 } catch (RemoteException e) { 352 e.rethrowFromSystemServer(); 353 } 354 } 355 return defaultValue; 356 } 357 358 /** 359 * Get group id for the given UUID 360 * @param uuid 361 * @return list of group IDs 362 * 363 * @hide 364 */ 365 @SystemApi 366 @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) getAllGroupIds(@ullable ParcelUuid uuid)367 public @NonNull List<Integer> getAllGroupIds(@Nullable ParcelUuid uuid) { 368 if (VDBG) log("getAllGroupIds()"); 369 final IBluetoothCsipSetCoordinator service = getService(); 370 final List<Integer> defaultValue = new ArrayList<>(); 371 if (service == null) { 372 Log.w(TAG, "Proxy not attached to service"); 373 if (DBG) log(Log.getStackTraceString(new Throwable())); 374 } else if (isEnabled()) { 375 try { 376 final SynchronousResultReceiver<List<Integer>> recv = 377 SynchronousResultReceiver.get(); 378 service.getAllGroupIds(uuid, mAttributionSource, recv); 379 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 380 } catch (TimeoutException e) { 381 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 382 } catch (RemoteException e) { 383 e.rethrowFromSystemServer(); 384 } 385 } 386 return defaultValue; 387 } 388 389 /** 390 * {@inheritDoc} 391 */ 392 @Override getConnectedDevices()393 public @NonNull List<BluetoothDevice> getConnectedDevices() { 394 if (VDBG) log("getConnectedDevices()"); 395 final IBluetoothCsipSetCoordinator service = getService(); 396 final List<BluetoothDevice> defaultValue = new ArrayList<>(); 397 if (service == null) { 398 Log.w(TAG, "Proxy not attached to service"); 399 if (DBG) log(Log.getStackTraceString(new Throwable())); 400 } else if (isEnabled()) { 401 try { 402 final SynchronousResultReceiver<List<BluetoothDevice>> recv = 403 SynchronousResultReceiver.get(); 404 service.getConnectedDevices(mAttributionSource, recv); 405 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 406 } catch (TimeoutException e) { 407 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 408 } catch (RemoteException e) { 409 e.rethrowFromSystemServer(); 410 } 411 } 412 return defaultValue; 413 } 414 415 /** 416 * {@inheritDoc} 417 */ 418 @Override 419 public getDevicesMatchingConnectionStates(@onNull int[] states)420 @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) { 421 if (VDBG) log("getDevicesMatchingStates(states=" + Arrays.toString(states) + ")"); 422 final IBluetoothCsipSetCoordinator service = getService(); 423 final List<BluetoothDevice> defaultValue = new ArrayList<>(); 424 if (service == null) { 425 Log.w(TAG, "Proxy not attached to service"); 426 if (DBG) log(Log.getStackTraceString(new Throwable())); 427 } else if (isEnabled()) { 428 try { 429 final SynchronousResultReceiver<List<BluetoothDevice>> recv = 430 SynchronousResultReceiver.get(); 431 service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv); 432 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 433 } catch (TimeoutException e) { 434 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 435 } catch (RemoteException e) { 436 e.rethrowFromSystemServer(); 437 } 438 } 439 return defaultValue; 440 } 441 442 /** 443 * {@inheritDoc} 444 */ 445 @Override 446 public getConnectionState(@ullable BluetoothDevice device)447 @BluetoothProfile.BtProfileState int getConnectionState(@Nullable BluetoothDevice device) { 448 if (VDBG) log("getState(" + device + ")"); 449 final IBluetoothCsipSetCoordinator service = getService(); 450 final int defaultValue = BluetoothProfile.STATE_DISCONNECTED; 451 if (service == null) { 452 Log.w(TAG, "Proxy not attached to service"); 453 if (DBG) log(Log.getStackTraceString(new Throwable())); 454 } else if (isEnabled()) { 455 try { 456 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get(); 457 service.getConnectionState(device, mAttributionSource, recv); 458 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 459 } catch (TimeoutException e) { 460 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 461 } catch (RemoteException e) { 462 e.rethrowFromSystemServer(); 463 } 464 } 465 return defaultValue; 466 } 467 468 /** 469 * Set connection policy of the profile 470 * 471 * <p> The device should already be paired. 472 * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED}, 473 * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN} 474 * 475 * @param device Paired bluetooth device 476 * @param connectionPolicy is the connection policy to set to for this profile 477 * @return true if connectionPolicy is set, false on error 478 * 479 * @hide 480 */ 481 @SystemApi 482 @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) setConnectionPolicy( @ullable BluetoothDevice device, @ConnectionPolicy int connectionPolicy)483 public boolean setConnectionPolicy( 484 @Nullable BluetoothDevice device, @ConnectionPolicy int connectionPolicy) { 485 if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")"); 486 final IBluetoothCsipSetCoordinator service = getService(); 487 final boolean defaultValue = false; 488 if (service == null) { 489 Log.w(TAG, "Proxy not attached to service"); 490 if (DBG) log(Log.getStackTraceString(new Throwable())); 491 } else if (isEnabled() && isValidDevice(device) 492 && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN 493 || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) { 494 try { 495 final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get(); 496 service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv); 497 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 498 } catch (TimeoutException e) { 499 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 500 } catch (RemoteException e) { 501 e.rethrowFromSystemServer(); 502 } 503 } 504 return defaultValue; 505 } 506 507 /** 508 * Get the connection policy of the profile. 509 * 510 * <p> The connection policy can be any of: 511 * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, 512 * {@link #CONNECTION_POLICY_UNKNOWN} 513 * 514 * @param device Bluetooth device 515 * @return connection policy of the device 516 * 517 * @hide 518 */ 519 @SystemApi 520 @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) getConnectionPolicy(@ullable BluetoothDevice device)521 public @ConnectionPolicy int getConnectionPolicy(@Nullable BluetoothDevice device) { 522 if (VDBG) log("getConnectionPolicy(" + device + ")"); 523 final IBluetoothCsipSetCoordinator service = getService(); 524 final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 525 if (service == null) { 526 Log.w(TAG, "Proxy not attached to service"); 527 if (DBG) log(Log.getStackTraceString(new Throwable())); 528 } else if (isEnabled() && isValidDevice(device)) { 529 try { 530 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get(); 531 service.getConnectionPolicy(device, mAttributionSource, recv); 532 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); 533 } catch (TimeoutException e) { 534 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 535 } catch (RemoteException e) { 536 e.rethrowFromSystemServer(); 537 } 538 } 539 return defaultValue; 540 } 541 isEnabled()542 private boolean isEnabled() { 543 return mAdapter.getState() == BluetoothAdapter.STATE_ON; 544 } 545 isValidDevice(@ullable BluetoothDevice device)546 private static boolean isValidDevice(@Nullable BluetoothDevice device) { 547 return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress()); 548 } 549 log(String msg)550 private static void log(String msg) { 551 Log.d(TAG, msg); 552 } 553 } 554