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