1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.bluetooth; 18 19 import android.Manifest; 20 import android.annotation.NonNull; 21 import android.annotation.RequiresNoPermission; 22 import android.annotation.RequiresPermission; 23 import android.annotation.SdkConstant; 24 import android.annotation.SdkConstant.SdkConstantType; 25 import android.annotation.SuppressLint; 26 import android.annotation.SystemApi; 27 import android.bluetooth.annotations.RequiresBluetoothConnectPermission; 28 import android.bluetooth.annotations.RequiresLegacyBluetoothPermission; 29 import android.compat.annotation.UnsupportedAppUsage; 30 import android.content.AttributionSource; 31 import android.content.Context; 32 import android.os.Build; 33 import android.os.IBinder; 34 import android.os.IpcDataCache; 35 import android.os.RemoteException; 36 import android.util.CloseGuard; 37 import android.util.Log; 38 import android.util.Pair; 39 40 import java.util.Collections; 41 import java.util.List; 42 import java.util.Objects; 43 44 /** 45 * This class provides the APIs to control the Bluetooth SIM Access Profile (SAP). 46 * 47 * <p>BluetoothSap is a proxy object for controlling the Bluetooth Service via IPC. Use {@link 48 * BluetoothAdapter#getProfileProxy} to get the BluetoothSap proxy object. 49 * 50 * <p>Each method is protected with its appropriate permission. 51 * 52 * @hide 53 */ 54 @SystemApi 55 public final class BluetoothSap implements BluetoothProfile, AutoCloseable { 56 57 private static final String TAG = "BluetoothSap"; 58 private static final boolean DBG = true; 59 private static final boolean VDBG = false; 60 61 private CloseGuard mCloseGuard; 62 63 /** 64 * Intent used to broadcast the change in connection state of the profile. 65 * 66 * <p>This intent will have 3 extras: 67 * 68 * <ul> 69 * <li>{@link #EXTRA_STATE} - The current state of the profile. 70 * <li>{@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. 71 * <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device. 72 * </ul> 73 * 74 * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of {@link 75 * #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, {@link #STATE_CONNECTED}, {@link 76 * #STATE_DISCONNECTING}. 77 * 78 * @hide 79 */ 80 @SystemApi 81 @RequiresLegacyBluetoothPermission 82 @RequiresBluetoothConnectPermission 83 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 84 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 85 @SuppressLint("ActionValue") 86 public static final String ACTION_CONNECTION_STATE_CHANGED = 87 "android.bluetooth.sap.profile.action.CONNECTION_STATE_CHANGED"; 88 89 /** 90 * There was an error trying to obtain the state. 91 * 92 * @hide 93 */ 94 public static final int STATE_ERROR = -1; 95 96 /** 97 * Connection state change succeeded. 98 * 99 * @hide 100 */ 101 public static final int RESULT_SUCCESS = 1; 102 103 /** 104 * Connection canceled before completion. 105 * 106 * @hide 107 */ 108 public static final int RESULT_CANCELED = 2; 109 110 private final BluetoothAdapter mAdapter; 111 private final AttributionSource mAttributionSource; 112 113 private IBluetoothSap mService; 114 115 /** Create a BluetoothSap proxy object. */ BluetoothSap(Context context, BluetoothAdapter adapter)116 /* package */ BluetoothSap(Context context, BluetoothAdapter adapter) { 117 if (DBG) Log.d(TAG, "Create BluetoothSap proxy object"); 118 mAdapter = adapter; 119 mAttributionSource = adapter.getAttributionSource(); 120 mService = null; 121 mCloseGuard = new CloseGuard(); 122 mCloseGuard.open("close"); 123 } 124 125 /** @hide */ 126 @SuppressWarnings("Finalize") // TODO(b/314811467) finalize()127 protected void finalize() { 128 if (mCloseGuard != null) { 129 mCloseGuard.warnIfOpen(); 130 } 131 close(); 132 } 133 134 /** 135 * Close the connection to the backing service. Other public functions of BluetoothSap will 136 * return default error results once close() has been called. Multiple invocations of close() 137 * are ok. 138 * 139 * @hide 140 */ 141 @Override close()142 public synchronized void close() { 143 mAdapter.closeProfileProxy(this); 144 } 145 146 /** @hide */ 147 @Override onServiceConnected(IBinder service)148 public void onServiceConnected(IBinder service) { 149 mService = IBluetoothSap.Stub.asInterface(service); 150 } 151 152 /** @hide */ 153 @Override onServiceDisconnected()154 public void onServiceDisconnected() { 155 mService = null; 156 } 157 getService()158 private IBluetoothSap getService() { 159 return mService; 160 } 161 162 /** @hide */ 163 @Override getAdapter()164 public BluetoothAdapter getAdapter() { 165 return mAdapter; 166 } 167 168 /** 169 * Get the current state of the BluetoothSap service. 170 * 171 * @return One of the STATE_ return codes, or STATE_ERROR if this proxy object is currently not 172 * connected to the Sap service. 173 * @hide 174 */ 175 @RequiresBluetoothConnectPermission 176 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getState()177 public int getState() { 178 if (VDBG) log("getState()"); 179 final IBluetoothSap service = getService(); 180 if (service == null) { 181 Log.w(TAG, "Proxy not attached to service"); 182 if (DBG) log(Log.getStackTraceString(new Throwable())); 183 } else if (isEnabled()) { 184 try { 185 return service.getState(mAttributionSource); 186 } catch (RemoteException e) { 187 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 188 } 189 } 190 return BluetoothSap.STATE_ERROR; 191 } 192 193 /** 194 * Get the currently connected remote Bluetooth device (PCE). 195 * 196 * @return The remote Bluetooth device, or null if not in connected or connecting state, or if 197 * this proxy object is not connected to the Sap service. 198 * @hide 199 */ 200 @RequiresBluetoothConnectPermission 201 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getClient()202 public BluetoothDevice getClient() { 203 if (VDBG) log("getClient()"); 204 final IBluetoothSap service = getService(); 205 if (service == null) { 206 Log.w(TAG, "Proxy not attached to service"); 207 if (DBG) log(Log.getStackTraceString(new Throwable())); 208 } else if (isEnabled()) { 209 try { 210 return Attributable.setAttributionSource( 211 service.getClient(mAttributionSource), mAttributionSource); 212 } catch (RemoteException e) { 213 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 214 } 215 } 216 return null; 217 } 218 219 /** 220 * Returns true if the specified Bluetooth device is connected. Returns false if not connected, 221 * or if this proxy object is not currently connected to the Sap service. 222 * 223 * @hide 224 */ 225 @RequiresBluetoothConnectPermission 226 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) isConnected(BluetoothDevice device)227 public boolean isConnected(BluetoothDevice device) { 228 if (VDBG) log("isConnected(" + device + ")"); 229 final IBluetoothSap service = getService(); 230 if (service == null) { 231 Log.w(TAG, "Proxy not attached to service"); 232 if (DBG) log(Log.getStackTraceString(new Throwable())); 233 } else if (isEnabled()) { 234 try { 235 return service.isConnected(device, mAttributionSource); 236 } catch (RemoteException e) { 237 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 238 } 239 } 240 return false; 241 } 242 243 /** 244 * Initiate connection. Initiation of outgoing connections is not supported for SAP server. 245 * 246 * @hide 247 */ 248 @RequiresNoPermission connect(BluetoothDevice device)249 public boolean connect(BluetoothDevice device) { 250 if (DBG) log("connect(" + device + ")" + "not supported for SAPS"); 251 return false; 252 } 253 254 /** 255 * Initiate disconnect. 256 * 257 * @param device Remote Bluetooth Device 258 * @return false on error, true otherwise 259 * @hide 260 */ 261 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 262 @RequiresBluetoothConnectPermission 263 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) disconnect(BluetoothDevice device)264 public boolean disconnect(BluetoothDevice device) { 265 if (DBG) log("disconnect(" + device + ")"); 266 final IBluetoothSap service = getService(); 267 if (service == null) { 268 Log.w(TAG, "Proxy not attached to service"); 269 if (DBG) log(Log.getStackTraceString(new Throwable())); 270 } else if (isEnabled() && isValidDevice(device)) { 271 try { 272 return service.disconnect(device, mAttributionSource); 273 } catch (RemoteException e) { 274 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 275 } 276 } 277 return false; 278 } 279 280 /** 281 * Get the list of connected devices. Currently at most one. 282 * 283 * @return list of connected devices 284 * @hide 285 */ 286 @RequiresBluetoothConnectPermission 287 @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) getConnectedDevices()288 public List<BluetoothDevice> getConnectedDevices() { 289 if (DBG) log("getConnectedDevices()"); 290 final IBluetoothSap service = getService(); 291 if (service == null) { 292 Log.w(TAG, "Proxy not attached to service"); 293 if (DBG) log(Log.getStackTraceString(new Throwable())); 294 } else if (isEnabled()) { 295 try { 296 return Attributable.setAttributionSource( 297 service.getConnectedDevices(mAttributionSource), mAttributionSource); 298 } catch (RemoteException e) { 299 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 300 } 301 } 302 return Collections.emptyList(); 303 } 304 305 /** 306 * Get the list of devices matching specified states. Currently at most one. 307 * 308 * @return list of matching devices 309 * @hide 310 */ 311 @RequiresBluetoothConnectPermission 312 @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) getDevicesMatchingConnectionStates(int[] states)313 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 314 if (DBG) log("getDevicesMatchingStates()"); 315 final IBluetoothSap service = getService(); 316 if (service == null) { 317 Log.w(TAG, "Proxy not attached to service"); 318 if (DBG) log(Log.getStackTraceString(new Throwable())); 319 } else if (isEnabled()) { 320 try { 321 return Attributable.setAttributionSource( 322 service.getDevicesMatchingConnectionStates(states, mAttributionSource), 323 mAttributionSource); 324 } catch (RemoteException e) { 325 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 326 } 327 } 328 return Collections.emptyList(); 329 } 330 331 /** 332 * There are several instances of IpcDataCache used in this class. BluetoothCache wraps up the 333 * common code. All caches are created with a maximum of eight entries, and the key is in the 334 * bluetooth module. The name is set to the api. 335 */ 336 private static class BluetoothCache<Q, R> extends IpcDataCache<Q, R> { BluetoothCache(String api, IpcDataCache.QueryHandler query)337 BluetoothCache(String api, IpcDataCache.QueryHandler query) { 338 super(8, IpcDataCache.MODULE_BLUETOOTH, api, api, query); 339 } 340 } 341 ; 342 343 /** @hide */ disableBluetoothGetConnectionStateCache()344 public void disableBluetoothGetConnectionStateCache() { 345 sBluetoothConnectionCache.disableForCurrentProcess(); 346 } 347 348 /** @hide */ invalidateBluetoothGetConnectionStateCache()349 public static void invalidateBluetoothGetConnectionStateCache() { 350 invalidateCache(GET_CONNECTION_STATE_API); 351 } 352 353 /** 354 * Invalidate a bluetooth cache. This method is just a short-hand wrapper that enforces the 355 * bluetooth module. 356 */ invalidateCache(@onNull String api)357 private static void invalidateCache(@NonNull String api) { 358 IpcDataCache.invalidateCache(IpcDataCache.MODULE_BLUETOOTH, api); 359 } 360 361 private static final IpcDataCache.QueryHandler< 362 Pair<IBinder, Pair<AttributionSource, BluetoothDevice>>, Integer> 363 sBluetoothConnectionQuery = 364 new IpcDataCache.QueryHandler<>() { 365 @RequiresBluetoothConnectPermission 366 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) 367 @Override 368 public Integer apply( 369 Pair<IBinder, Pair<AttributionSource, BluetoothDevice>> pairQuery) { 370 IBluetoothSap service = IBluetoothSap.Stub.asInterface(pairQuery.first); 371 AttributionSource source = pairQuery.second.first; 372 BluetoothDevice device = pairQuery.second.second; 373 if (DBG) { 374 log( 375 "getConnectionState(" 376 + device.getAnonymizedAddress() 377 + ") uncached"); 378 } 379 try { 380 return service.getConnectionState(device, source); 381 } catch (RemoteException e) { 382 throw e.rethrowAsRuntimeException(); 383 } 384 } 385 }; 386 387 private static final String GET_CONNECTION_STATE_API = "BluetoothSap_getConnectionState"; 388 389 private static final BluetoothCache< 390 Pair<IBinder, Pair<AttributionSource, BluetoothDevice>>, Integer> 391 sBluetoothConnectionCache = 392 new BluetoothCache<>(GET_CONNECTION_STATE_API, sBluetoothConnectionQuery); 393 394 /** 395 * Get connection state of device 396 * 397 * @return device connection state 398 * @hide 399 */ 400 @RequiresBluetoothConnectPermission 401 @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) getConnectionState(BluetoothDevice device)402 public int getConnectionState(BluetoothDevice device) { 403 if (DBG) log("getConnectionState(" + device + ")"); 404 final IBluetoothSap service = getService(); 405 if (service == null) { 406 Log.w(TAG, "BT not enabled. Cannot get connection state"); 407 if (DBG) log(Log.getStackTraceString(new Throwable())); 408 } else if (isEnabled() && isValidDevice(device)) { 409 try { 410 return sBluetoothConnectionCache.query( 411 new Pair<>(service.asBinder(), new Pair<>(mAttributionSource, device))); 412 } catch (RuntimeException e) { 413 if (!(e.getCause() instanceof RemoteException)) { 414 throw e; 415 } 416 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 417 } 418 } 419 return BluetoothProfile.STATE_DISCONNECTED; 420 } 421 422 /** 423 * Set priority of the profile 424 * 425 * <p>The device should already be paired. Priority can be one of {@link #PRIORITY_ON} or {@link 426 * #PRIORITY_OFF}, 427 * 428 * @param device Paired bluetooth device 429 * @return true if priority is set, false on error 430 * @hide 431 */ 432 @RequiresBluetoothConnectPermission 433 @RequiresPermission( 434 allOf = { 435 android.Manifest.permission.BLUETOOTH_CONNECT, 436 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 437 }) setPriority(BluetoothDevice device, int priority)438 public boolean setPriority(BluetoothDevice device, int priority) { 439 if (DBG) log("setPriority(" + device + ", " + priority + ")"); 440 return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority)); 441 } 442 443 /** 444 * Set connection policy of the profile 445 * 446 * <p>The device should already be paired. Connection policy can be one of {@link 447 * #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, {@link 448 * #CONNECTION_POLICY_UNKNOWN} 449 * 450 * @param device Paired bluetooth device 451 * @param connectionPolicy is the connection policy to set to for this profile 452 * @return true if connectionPolicy is set, false on error 453 * @throws NullPointerException if device is null 454 * @hide 455 */ 456 @SystemApi 457 @RequiresBluetoothConnectPermission 458 @RequiresPermission( 459 allOf = { 460 android.Manifest.permission.BLUETOOTH_CONNECT, 461 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 462 }) setConnectionPolicy( @onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)463 public boolean setConnectionPolicy( 464 @NonNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy) { 465 if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")"); 466 Objects.requireNonNull(device, "BluetoothDevice cannot be null"); 467 final IBluetoothSap service = getService(); 468 if (service == null) { 469 Log.w(TAG, "Proxy not attached to service"); 470 if (DBG) log(Log.getStackTraceString(new Throwable())); 471 } else if (isEnabled() 472 && isValidDevice(device) 473 && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN 474 || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) { 475 try { 476 return service.setConnectionPolicy(device, connectionPolicy, mAttributionSource); 477 } catch (RemoteException e) { 478 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 479 } 480 } 481 return false; 482 } 483 484 /** 485 * Get the priority of the profile. 486 * 487 * <p>The priority can be any of: {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link 488 * #PRIORITY_UNDEFINED} 489 * 490 * @param device Bluetooth device 491 * @return priority of the device 492 * @hide 493 */ 494 @RequiresBluetoothConnectPermission 495 @RequiresPermission( 496 allOf = { 497 android.Manifest.permission.BLUETOOTH_CONNECT, 498 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 499 }) getPriority(BluetoothDevice device)500 public int getPriority(BluetoothDevice device) { 501 if (VDBG) log("getPriority(" + device + ")"); 502 return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device)); 503 } 504 505 /** 506 * Get the connection policy of the profile. 507 * 508 * <p>The connection policy can be any of: {@link #CONNECTION_POLICY_ALLOWED}, {@link 509 * #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN} 510 * 511 * @param device Bluetooth device 512 * @return connection policy of the device 513 * @throws NullPointerException if device is null 514 * @hide 515 */ 516 @SystemApi 517 @RequiresBluetoothConnectPermission 518 @RequiresPermission( 519 allOf = { 520 android.Manifest.permission.BLUETOOTH_CONNECT, 521 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 522 }) getConnectionPolicy(@onNull BluetoothDevice device)523 public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) { 524 if (VDBG) log("getConnectionPolicy(" + device + ")"); 525 Objects.requireNonNull(device, "BluetoothDevice cannot be null"); 526 final IBluetoothSap service = getService(); 527 if (service == null) { 528 Log.w(TAG, "Proxy not attached to service"); 529 if (DBG) log(Log.getStackTraceString(new Throwable())); 530 } else if (isEnabled() && isValidDevice(device)) { 531 try { 532 return service.getConnectionPolicy(device, mAttributionSource); 533 } catch (RemoteException e) { 534 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); 535 } 536 } 537 return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; 538 } 539 log(String msg)540 private static void log(String msg) { 541 Log.d(TAG, msg); 542 } 543 isEnabled()544 private boolean isEnabled() { 545 return mAdapter.isEnabled(); 546 } 547 isValidDevice(BluetoothDevice device)548 private static boolean isValidDevice(BluetoothDevice device) { 549 return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress()); 550 } 551 } 552