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