1 package org.robolectric.shadows; 2 3 import static android.bluetooth.BluetoothAdapter.STATE_ON; 4 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; 5 import static android.os.Build.VERSION_CODES.LOLLIPOP; 6 import static android.os.Build.VERSION_CODES.M; 7 import static android.os.Build.VERSION_CODES.O; 8 import static android.os.Build.VERSION_CODES.Q; 9 import static android.os.Build.VERSION_CODES.R; 10 import static android.os.Build.VERSION_CODES.S_V2; 11 import static android.os.Build.VERSION_CODES.TIRAMISU; 12 import static org.robolectric.Shadows.shadowOf; 13 import static org.robolectric.util.reflector.Reflector.reflector; 14 15 import android.app.PendingIntent; 16 import android.app.PendingIntent.CanceledException; 17 import android.bluetooth.BluetoothAdapter; 18 import android.bluetooth.BluetoothAdapter.LeScanCallback; 19 import android.bluetooth.BluetoothDevice; 20 import android.bluetooth.BluetoothProfile; 21 import android.bluetooth.BluetoothServerSocket; 22 import android.bluetooth.BluetoothSocket; 23 import android.bluetooth.BluetoothStatusCodes; 24 import android.bluetooth.IBluetoothManager; 25 import android.bluetooth.IBluetoothProfileServiceConnection; 26 import android.bluetooth.le.BluetoothLeAdvertiser; 27 import android.bluetooth.le.BluetoothLeScanner; 28 import android.content.AttributionSource; 29 import android.content.Context; 30 import android.os.Build; 31 import android.os.Build.VERSION_CODES; 32 import android.os.ParcelUuid; 33 import android.os.RemoteException; 34 import android.provider.Settings; 35 import com.google.common.collect.ImmutableList; 36 import java.io.IOException; 37 import java.time.Duration; 38 import java.util.ArrayList; 39 import java.util.Collections; 40 import java.util.HashMap; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Set; 45 import java.util.UUID; 46 import java.util.concurrent.ConcurrentHashMap; 47 import java.util.concurrent.ConcurrentMap; 48 import javax.annotation.Nullable; 49 import org.robolectric.RuntimeEnvironment; 50 import org.robolectric.annotation.Implementation; 51 import org.robolectric.annotation.Implements; 52 import org.robolectric.annotation.RealObject; 53 import org.robolectric.annotation.Resetter; 54 import org.robolectric.util.ReflectionHelpers; 55 import org.robolectric.util.ReflectionHelpers.ClassParameter; 56 import org.robolectric.util.reflector.Accessor; 57 import org.robolectric.util.reflector.Direct; 58 import org.robolectric.util.reflector.ForType; 59 import org.robolectric.util.reflector.Static; 60 61 @SuppressWarnings({"UnusedDeclaration"}) 62 @Implements(value = BluetoothAdapter.class, looseSignatures = true) 63 public class ShadowBluetoothAdapter { 64 @RealObject private BluetoothAdapter realAdapter; 65 66 private static final int ADDRESS_LENGTH = 17; 67 private static final int LE_MAXIMUM_ADVERTISING_DATA_LENGTH = 31; 68 private static final int LE_MAXIMUM_ADVERTISING_DATA_LENGTH_EXTENDED = 1650; 69 70 /** 71 * Equivalent value to internal SystemApi {@link 72 * BluetoothStatusCodes#RFCOMM_LISTENER_START_FAILED_UUID_IN_USE}. 73 */ 74 public static final int RFCOMM_LISTENER_START_FAILED_UUID_IN_USE = 2000; 75 76 /** 77 * Equivalent value to internal SystemApi {@link 78 * BluetoothStatusCodes#RFCOMM_LISTENER_OPERATION_FAILED_NO_MATCHING_SERVICE_RECORD}. 79 */ 80 public static final int RFCOMM_LISTENER_OPERATION_FAILED_NO_MATCHING_SERVICE_RECORD = 2001; 81 82 /** 83 * Equivalent value to internal SystemApi {@link 84 * BluetoothStatusCodes#RFCOMM_LISTENER_FAILED_TO_CLOSE_SERVER_SOCKET}. 85 */ 86 public static final int RFCOMM_LISTENER_FAILED_TO_CLOSE_SERVER_SOCKET = 2004; 87 88 private static boolean isBluetoothSupported = true; 89 90 private static final Map<String, BluetoothDevice> deviceCache = new HashMap<>(); 91 private Set<BluetoothDevice> bondedDevices = new HashSet<BluetoothDevice>(); 92 private Set<LeScanCallback> leScanCallbacks = new HashSet<LeScanCallback>(); 93 private boolean isDiscovering; 94 private String address; 95 private int state; 96 private String name = "DefaultBluetoothDeviceName"; 97 private int scanMode = BluetoothAdapter.SCAN_MODE_NONE; 98 private Duration discoverableTimeout; 99 private boolean isBleScanAlwaysAvailable = true; 100 private boolean isMultipleAdvertisementSupported = true; 101 private boolean isLeExtendedAdvertisingSupported = true; 102 private boolean isOverridingProxyBehavior; 103 private final Map<Integer, Integer> profileConnectionStateData = new HashMap<>(); 104 private final Map<Integer, BluetoothProfile> profileProxies = new HashMap<>(); 105 private final ConcurrentMap<UUID, BackgroundRfcommServerEntry> backgroundRfcommServers = 106 new ConcurrentHashMap<>(); 107 private final Map<Integer, List<BluetoothProfile.ServiceListener>> 108 bluetoothProfileServiceListeners = new HashMap<>(); 109 110 @Resetter reset()111 public static void reset() { 112 setIsBluetoothSupported(true); 113 BluetoothAdapterReflector bluetoothReflector = reflector(BluetoothAdapterReflector.class); 114 int apiLevel = RuntimeEnvironment.getApiLevel(); 115 if (apiLevel >= VERSION_CODES.LOLLIPOP && apiLevel <= VERSION_CODES.R) { 116 bluetoothReflector.setSBluetoothLeAdvertiser(null); 117 bluetoothReflector.setSBluetoothLeScanner(null); 118 } 119 bluetoothReflector.setAdapter(null); 120 deviceCache.clear(); 121 } 122 123 @Implementation getDefaultAdapter()124 protected static BluetoothAdapter getDefaultAdapter() { 125 if (!isBluetoothSupported) { 126 return null; 127 } 128 return reflector(BluetoothAdapterReflector.class).getDefaultAdapter(); 129 } 130 131 /** Requires LooseSignatures because of {@link AttributionSource} parameter */ 132 @Implementation(minSdk = VERSION_CODES.TIRAMISU) createAdapter(Object attributionSource)133 protected static Object createAdapter(Object attributionSource) { 134 IBluetoothManager service = 135 ReflectionHelpers.createDelegatingProxy( 136 IBluetoothManager.class, new BluetoothManagerDelegate()); 137 return ReflectionHelpers.callConstructor( 138 BluetoothAdapter.class, 139 ClassParameter.from(IBluetoothManager.class, service), 140 ClassParameter.from(AttributionSource.class, attributionSource)); 141 } 142 143 /** Determines if getDefaultAdapter() returns the default local adapter (true) or null (false). */ setIsBluetoothSupported(boolean supported)144 public static void setIsBluetoothSupported(boolean supported) { 145 isBluetoothSupported = supported; 146 } 147 148 /** @deprecated use real BluetoothLeAdvertiser instead */ 149 @Deprecated setBluetoothLeAdvertiser(BluetoothLeAdvertiser advertiser)150 public void setBluetoothLeAdvertiser(BluetoothLeAdvertiser advertiser) { 151 if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.LOLLIPOP_MR1) { 152 reflector(BluetoothAdapterReflector.class, realAdapter).setSBluetoothLeAdvertiser(advertiser); 153 } else { 154 reflector(BluetoothAdapterReflector.class, realAdapter).setBluetoothLeAdvertiser(advertiser); 155 } 156 } 157 158 @Implementation getRemoteDevice(String address)159 protected synchronized BluetoothDevice getRemoteDevice(String address) { 160 if (!deviceCache.containsKey(address)) { 161 deviceCache.put( 162 address, 163 reflector(BluetoothAdapterReflector.class, realAdapter).getRemoteDevice(address)); 164 } 165 return deviceCache.get(address); 166 } 167 168 @Implementation getBondedDevices()169 protected Set<BluetoothDevice> getBondedDevices() { 170 return Collections.unmodifiableSet(bondedDevices); 171 } 172 setBondedDevices(Set<BluetoothDevice> bluetoothDevices)173 public void setBondedDevices(Set<BluetoothDevice> bluetoothDevices) { 174 bondedDevices = bluetoothDevices; 175 } 176 177 @Implementation listenUsingInsecureRfcommWithServiceRecord( String serviceName, UUID uuid)178 protected BluetoothServerSocket listenUsingInsecureRfcommWithServiceRecord( 179 String serviceName, UUID uuid) { 180 return ShadowBluetoothServerSocket.newInstance( 181 BluetoothSocket.TYPE_RFCOMM, /*auth=*/ false, /*encrypt=*/ false, new ParcelUuid(uuid)); 182 } 183 184 @Implementation listenUsingRfcommWithServiceRecord(String serviceName, UUID uuid)185 protected BluetoothServerSocket listenUsingRfcommWithServiceRecord(String serviceName, UUID uuid) 186 throws IOException { 187 return ShadowBluetoothServerSocket.newInstance( 188 BluetoothSocket.TYPE_RFCOMM, /*auth=*/ false, /*encrypt=*/ true, new ParcelUuid(uuid)); 189 } 190 191 @Implementation(minSdk = Q) listenUsingInsecureL2capChannel()192 protected BluetoothServerSocket listenUsingInsecureL2capChannel() throws IOException { 193 return ShadowBluetoothServerSocket.newInstance( 194 BluetoothSocket.TYPE_L2CAP, /*auth=*/ false, /*encrypt=*/ true, /*uuid=*/ null); 195 } 196 197 @Implementation(minSdk = Q) listenUsingL2capChannel()198 protected BluetoothServerSocket listenUsingL2capChannel() throws IOException { 199 return ShadowBluetoothServerSocket.newInstance( 200 BluetoothSocket.TYPE_L2CAP, /*auth=*/ false, /*encrypt=*/ true, /*uuid=*/ null); 201 } 202 203 @Implementation startDiscovery()204 protected boolean startDiscovery() { 205 isDiscovering = true; 206 return true; 207 } 208 209 @Implementation cancelDiscovery()210 protected boolean cancelDiscovery() { 211 isDiscovering = false; 212 return true; 213 } 214 215 /** When true, overrides the value of {@link #getLeState}. By default, this is false. */ 216 @Implementation(minSdk = M) isBleScanAlwaysAvailable()217 protected boolean isBleScanAlwaysAvailable() { 218 return isBleScanAlwaysAvailable; 219 } 220 221 /** 222 * Decides the correct LE state. When off, BLE calls will fail or return null. 223 * 224 * <p>LE is enabled if either Bluetooth or BLE scans are enabled. LE is always off if Airplane 225 * Mode is enabled. 226 */ 227 @Implementation(minSdk = M) getLeState()228 public int getLeState() { 229 if (isAirplaneMode()) { 230 return BluetoothAdapter.STATE_OFF; 231 } 232 233 if (isEnabled()) { 234 return BluetoothAdapter.STATE_ON; 235 } 236 237 if (isBleScanAlwaysAvailable()) { 238 return BluetoothAdapter.STATE_BLE_ON; 239 } 240 241 return BluetoothAdapter.STATE_OFF; 242 } 243 244 /** 245 * True if either Bluetooth is enabled or BLE scanning is available. Always false if Airplane Mode 246 * is enabled. When false, BLE scans will fail. @Implementation(minSdk = M) protected boolean 247 * isLeEnabled() { if (isAirplaneMode()) { return false; } return isEnabled() || 248 * isBleScanAlwaysAvailable(); } 249 */ isAirplaneMode()250 private static boolean isAirplaneMode() { 251 Context context = RuntimeEnvironment.getApplication(); 252 return Settings.Global.getInt(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) 253 != 0; 254 } 255 256 @Implementation(minSdk = JELLY_BEAN_MR2) startLeScan(LeScanCallback callback)257 protected boolean startLeScan(LeScanCallback callback) { 258 return startLeScan(null, callback); 259 } 260 261 @Implementation(minSdk = JELLY_BEAN_MR2) startLeScan(UUID[] serviceUuids, LeScanCallback callback)262 protected boolean startLeScan(UUID[] serviceUuids, LeScanCallback callback) { 263 if (Build.VERSION.SDK_INT >= M && !realAdapter.isLeEnabled()) { 264 return false; 265 } 266 267 // Ignoring the serviceUuids param for now. 268 leScanCallbacks.add(callback); 269 return true; 270 } 271 272 @Implementation(minSdk = JELLY_BEAN_MR2) stopLeScan(LeScanCallback callback)273 protected void stopLeScan(LeScanCallback callback) { 274 leScanCallbacks.remove(callback); 275 } 276 getLeScanCallbacks()277 public Set<LeScanCallback> getLeScanCallbacks() { 278 return Collections.unmodifiableSet(leScanCallbacks); 279 } 280 getSingleLeScanCallback()281 public LeScanCallback getSingleLeScanCallback() { 282 if (leScanCallbacks.size() != 1) { 283 throw new IllegalStateException("There are " + leScanCallbacks.size() + " callbacks"); 284 } 285 return leScanCallbacks.iterator().next(); 286 } 287 288 @Implementation isDiscovering()289 protected boolean isDiscovering() { 290 return isDiscovering; 291 } 292 293 @Implementation isEnabled()294 protected boolean isEnabled() { 295 return state == BluetoothAdapter.STATE_ON; 296 } 297 298 @Implementation enable()299 protected boolean enable() { 300 setState(BluetoothAdapter.STATE_ON); 301 return true; 302 } 303 304 @Implementation disable()305 protected boolean disable() { 306 setState(BluetoothAdapter.STATE_OFF); 307 return true; 308 } 309 310 @Implementation disable(boolean persist)311 protected boolean disable(boolean persist) { 312 return disable(); 313 } 314 315 @Implementation getAddress()316 protected String getAddress() { 317 return this.address; 318 } 319 320 @Implementation getState()321 protected int getState() { 322 return state; 323 } 324 325 @Implementation getName()326 protected String getName() { 327 return name; 328 } 329 330 @Implementation setName(String name)331 protected boolean setName(String name) { 332 this.name = name; 333 return true; 334 } 335 336 /** 337 * Needs looseSignatures because in Android T the return value of this method was changed from 338 * bool to int. 339 */ 340 @Implementation setScanMode(int scanMode)341 protected Object setScanMode(int scanMode) { 342 boolean result = true; 343 if (scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE 344 && scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE 345 && scanMode != BluetoothAdapter.SCAN_MODE_NONE) { 346 result = false; 347 } 348 349 this.scanMode = scanMode; 350 if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) { 351 return result ? BluetoothStatusCodes.SUCCESS : BluetoothStatusCodes.ERROR_UNKNOWN; 352 } else { 353 return result; 354 } 355 } 356 357 @Implementation(maxSdk = Q) setScanMode(int scanMode, int discoverableTimeout)358 protected boolean setScanMode(int scanMode, int discoverableTimeout) { 359 setDiscoverableTimeout(discoverableTimeout); 360 return (boolean) setScanMode(scanMode); 361 } 362 363 @Implementation(minSdk = R, maxSdk = S_V2) setScanMode(int scanMode, long durationMillis)364 protected boolean setScanMode(int scanMode, long durationMillis) { 365 int durationSeconds = Math.toIntExact(durationMillis / 1000); 366 setDiscoverableTimeout(durationSeconds); 367 return (boolean) setScanMode(scanMode); 368 } 369 370 @Implementation getScanMode()371 protected int getScanMode() { 372 return scanMode; 373 } 374 375 /** 376 * Needs looseSignatures because the return value changed from {@code int} to {@link Duration} 377 * starting in T. 378 */ 379 @Implementation getDiscoverableTimeout()380 protected Object getDiscoverableTimeout() { 381 if (RuntimeEnvironment.getApiLevel() <= S_V2) { 382 return (int) discoverableTimeout.toSeconds(); 383 } else { 384 return discoverableTimeout; 385 } 386 } 387 388 @Implementation(maxSdk = S_V2) setDiscoverableTimeout(int timeout)389 protected void setDiscoverableTimeout(int timeout) { 390 discoverableTimeout = Duration.ofSeconds(timeout); 391 } 392 393 @Implementation(minSdk = 33) setDiscoverableTimeout(Duration timeout)394 protected int setDiscoverableTimeout(Duration timeout) { 395 if (getState() != STATE_ON) { 396 return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED; 397 } 398 if (timeout.toSeconds() > Integer.MAX_VALUE) { 399 throw new IllegalArgumentException( 400 "Timeout in seconds must be less or equal to " + Integer.MAX_VALUE); 401 } 402 this.discoverableTimeout = timeout; 403 return BluetoothStatusCodes.SUCCESS; 404 } 405 406 @Implementation(minSdk = LOLLIPOP) isMultipleAdvertisementSupported()407 protected boolean isMultipleAdvertisementSupported() { 408 return isMultipleAdvertisementSupported; 409 } 410 411 /** 412 * Validate a Bluetooth address, such as "00:43:A8:23:10:F0" Alphabetic characters must be 413 * uppercase to be valid. 414 * 415 * @param address Bluetooth address as string 416 * @return true if the address is valid, false otherwise 417 */ 418 @Implementation checkBluetoothAddress(String address)419 protected static boolean checkBluetoothAddress(String address) { 420 if (address == null || address.length() != ADDRESS_LENGTH) { 421 return false; 422 } 423 for (int i = 0; i < ADDRESS_LENGTH; i++) { 424 char c = address.charAt(i); 425 switch (i % 3) { 426 case 0: 427 case 1: 428 if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')) { 429 // hex character, OK 430 break; 431 } 432 return false; 433 case 2: 434 if (c == ':') { 435 break; // OK 436 } 437 return false; 438 } 439 } 440 return true; 441 } 442 443 /** 444 * Returns the connection state for the given Bluetooth {@code profile}, defaulting to {@link 445 * BluetoothProfile.STATE_DISCONNECTED} if the profile's connection state was never set. 446 * 447 * <p>Set a Bluetooth profile's connection state via {@link #setProfileConnectionState(int, int)}. 448 */ 449 @Implementation getProfileConnectionState(int profile)450 protected int getProfileConnectionState(int profile) { 451 Integer state = profileConnectionStateData.get(profile); 452 if (state == null) { 453 return BluetoothProfile.STATE_DISCONNECTED; 454 } 455 return state; 456 } 457 setAddress(String address)458 public void setAddress(String address) { 459 this.address = address; 460 } 461 setState(int state)462 public void setState(int state) { 463 this.state = state; 464 } 465 466 /** @deprecated Use {@link BluetoothAdapter#enable()} or {@link BluetoothAdapter#disable()}. */ 467 @Deprecated setEnabled(boolean enabled)468 public void setEnabled(boolean enabled) { 469 if (enabled) { 470 enable(); 471 } else { 472 disable(); 473 } 474 } 475 476 /** 477 * Sets the value for {@link isBleScanAlwaysAvailable}. If true, {@link getLeState} will always 478 * return true. 479 */ setBleScanAlwaysAvailable(boolean alwaysAvailable)480 public void setBleScanAlwaysAvailable(boolean alwaysAvailable) { 481 isBleScanAlwaysAvailable = alwaysAvailable; 482 } 483 484 /** Sets the value for {@link isMultipleAdvertisementSupported}. */ setIsMultipleAdvertisementSupported(boolean supported)485 public void setIsMultipleAdvertisementSupported(boolean supported) { 486 isMultipleAdvertisementSupported = supported; 487 } 488 489 /** Sets the connection state {@code state} for the given BluetoothProfile {@code profile} */ setProfileConnectionState(int profile, int state)490 public void setProfileConnectionState(int profile, int state) { 491 profileConnectionStateData.put(profile, state); 492 } 493 494 /** 495 * Sets the active BluetoothProfile {@code proxy} for the given {@code profile}. Will always 496 * affect behavior of {@link BluetoothAdapter#getProfileProxy} and {@link 497 * BluetoothAdapter#closeProfileProxy}. Call to {@link BluetoothAdapter#closeProfileProxy} can 498 * remove the set active proxy. 499 * 500 * @param proxy can be 'null' to simulate the situation where {@link 501 * BluetoothAdapter#getProfileProxy} would return 'false'. This can happen on older Android 502 * versions for Bluetooth profiles introduced in later Android versions. 503 */ setProfileProxy(int profile, @Nullable BluetoothProfile proxy)504 public void setProfileProxy(int profile, @Nullable BluetoothProfile proxy) { 505 isOverridingProxyBehavior = true; 506 if (proxy != null) { 507 profileProxies.put(profile, proxy); 508 } 509 } 510 511 /** 512 * @return 'true' if active (non-null) proxy has been set by {@link 513 * ShadowBluetoothAdapter#setProfileProxy} for the given {@code profile} AND it has not been 514 * "deactivated" by a call to {@link BluetoothAdapter#closeProfileProxy}. Only meaningful if 515 * {@link ShadowBluetoothAdapter#setProfileProxy} has been previously called. 516 */ hasActiveProfileProxy(int profile)517 public boolean hasActiveProfileProxy(int profile) { 518 return profileProxies.get(profile) != null; 519 } 520 521 /** 522 * Overrides behavior of {@link getProfileProxy} if {@link ShadowBluetoothAdapter#setProfileProxy} 523 * has been previously called. 524 * 525 * <p>If active (non-null) proxy has been set by {@link setProfileProxy} for the given {@code 526 * profile}, {@link getProfileProxy} will immediately call {@code onServiceConnected} of the given 527 * BluetoothProfile.ServiceListener {@code listener}. 528 * 529 * @return 'true' if a proxy object has been set by {@link setProfileProxy} for the given 530 * BluetoothProfile {@code profile} 531 */ 532 @Implementation getProfileProxy( Context context, BluetoothProfile.ServiceListener listener, int profile)533 protected boolean getProfileProxy( 534 Context context, BluetoothProfile.ServiceListener listener, int profile) { 535 if (!isOverridingProxyBehavior) { 536 return reflector(BluetoothAdapterReflector.class, realAdapter) 537 .getProfileProxy(context, listener, profile); 538 } 539 540 BluetoothProfile proxy = profileProxies.get(profile); 541 if (proxy == null) { 542 return false; 543 } else { 544 listener.onServiceConnected(profile, proxy); 545 List<BluetoothProfile.ServiceListener> profileListeners = 546 bluetoothProfileServiceListeners.get(profile); 547 if (profileListeners != null) { 548 profileListeners.add(listener); 549 } else { 550 bluetoothProfileServiceListeners.put(profile, new ArrayList<>(ImmutableList.of(listener))); 551 } 552 return true; 553 } 554 } 555 556 /** 557 * Overrides behavior of {@link closeProfileProxy} if {@link 558 * ShadowBluetoothAdapter#setProfileProxy} has been previously called. 559 * 560 * If the given non-null BluetoothProfile {@code proxy} was previously set for the given {@code 561 * profile} by {@link ShadowBluetoothAdapter#setProfileProxy}, this proxy will be "deactivated". 562 */ 563 @Implementation closeProfileProxy(int profile, BluetoothProfile proxy)564 protected void closeProfileProxy(int profile, BluetoothProfile proxy) { 565 if (!isOverridingProxyBehavior) { 566 reflector(BluetoothAdapterReflector.class, realAdapter).closeProfileProxy(profile, proxy); 567 return; 568 } 569 570 if (proxy != null && proxy.equals(profileProxies.get(profile))) { 571 profileProxies.remove(profile); 572 List<BluetoothProfile.ServiceListener> profileListeners = 573 bluetoothProfileServiceListeners.remove(profile); 574 if (profileListeners != null) { 575 for (BluetoothProfile.ServiceListener listener : profileListeners) { 576 listener.onServiceDisconnected(profile); 577 } 578 } 579 } 580 } 581 582 /** Returns the last value of {@link #setIsLeExtendedAdvertisingSupported}, defaulting to true. */ 583 @Implementation(minSdk = O) isLeExtendedAdvertisingSupported()584 protected boolean isLeExtendedAdvertisingSupported() { 585 return isLeExtendedAdvertisingSupported; 586 } 587 588 /** 589 * Sets the isLeExtendedAdvertisingSupported to enable/disable LE extended advertisements feature 590 */ setIsLeExtendedAdvertisingSupported(boolean supported)591 public void setIsLeExtendedAdvertisingSupported(boolean supported) { 592 isLeExtendedAdvertisingSupported = supported; 593 } 594 595 @Implementation(minSdk = O) getLeMaximumAdvertisingDataLength()596 protected int getLeMaximumAdvertisingDataLength() { 597 return isLeExtendedAdvertisingSupported 598 ? LE_MAXIMUM_ADVERTISING_DATA_LENGTH_EXTENDED 599 : LE_MAXIMUM_ADVERTISING_DATA_LENGTH; 600 } 601 602 @Implementation(minSdk = TIRAMISU) startRfcommServer(String name, UUID uuid, PendingIntent pendingIntent)603 protected int startRfcommServer(String name, UUID uuid, PendingIntent pendingIntent) { 604 // PendingIntent#isImmutable throws an NPE if the component does not exist, so verify directly 605 // against the flags for now. 606 if ((shadowOf(pendingIntent).getFlags() & PendingIntent.FLAG_IMMUTABLE) == 0) { 607 throw new IllegalArgumentException("RFCOMM server PendingIntent must be marked immutable"); 608 } 609 610 boolean[] isNewServerSocket = {false}; 611 backgroundRfcommServers.computeIfAbsent( 612 uuid, 613 unused -> { 614 isNewServerSocket[0] = true; 615 return new BackgroundRfcommServerEntry(uuid, pendingIntent); 616 }); 617 return isNewServerSocket[0] 618 ? BluetoothStatusCodes.SUCCESS 619 : RFCOMM_LISTENER_START_FAILED_UUID_IN_USE; 620 } 621 622 @Implementation(minSdk = TIRAMISU) stopRfcommServer(UUID uuid)623 protected int stopRfcommServer(UUID uuid) { 624 BackgroundRfcommServerEntry entry = backgroundRfcommServers.remove(uuid); 625 626 if (entry == null) { 627 return RFCOMM_LISTENER_OPERATION_FAILED_NO_MATCHING_SERVICE_RECORD; 628 } 629 630 try { 631 entry.serverSocket.close(); 632 return BluetoothStatusCodes.SUCCESS; 633 } catch (IOException e) { 634 return RFCOMM_LISTENER_FAILED_TO_CLOSE_SERVER_SOCKET; 635 } 636 } 637 638 @Implementation(minSdk = TIRAMISU) 639 @Nullable retrieveConnectedRfcommSocket(UUID uuid)640 protected BluetoothSocket retrieveConnectedRfcommSocket(UUID uuid) { 641 BackgroundRfcommServerEntry serverEntry = backgroundRfcommServers.get(uuid); 642 643 try { 644 return serverEntry == null ? null : serverEntry.serverSocket.accept(/* timeout= */ 0); 645 } catch (IOException e) { 646 // This means there is no pending socket, so the contract indicates we should return null. 647 return null; 648 } 649 } 650 651 /** 652 * Creates an incoming socket connection from the given {@link BluetoothDevice} to a background 653 * Bluetooth server created with {@link BluetoothAdapter#startRfcommServer(String, UUID, 654 * PendingIntent)} on the given uuid. 655 * 656 * <p>Creating this socket connection will invoke the {@link PendingIntent} provided in {@link 657 * BluetoothAdapter#startRfcommServer(String, UUID, PendingIntent)} when the server socket was 658 * created for the given UUID. The component provided in the intent can then call {@link 659 * BluetoothAdapter#retrieveConnectedRfcommSocket(UUID)} to obtain the server side socket. 660 * 661 * <p>A {@link ShadowBluetoothSocket} obtained from the returned {@link BluetoothSocket} can be 662 * used to send data to and receive data from the server side socket. This returned {@link 663 * BluetoothSocket} is the same socket as returned by {@link 664 * BluetoothAdapter#retrieveConnectedRfcommSocket(UUID)} and should generally not be used directly 665 * outside of obtaining the shadow, as this socket is normally not exposed outside of the 666 * component started by the pending intent. {@link ShadowBluetoothSocket#getInputStreamFeeder()} 667 * and {@link ShadowBluetoothSocket#getOutputStreamSink()} can be used to send data to and from 668 * the socket as if it was a remote connection. 669 * 670 * <p><b>Warning:</b> The socket returned by this method and the corresponding server side socket 671 * retrieved from {@link BluetoothAdapter#retrieveConnectedRfcommSocket(UUID)} do not support 672 * reads and writes from different threads. Once reading or writing is started for a given socket 673 * on a given thread, that type of operation on that socket must only be done on that thread. 674 * 675 * @return a server side BluetoothSocket or {@code null} if the {@link UUID} is not registered. 676 * This value should generally not be used directly, and is mainly used to obtain a shadow 677 * with which a RFCOMM client can be simulated. 678 * @throws IllegalArgumentException if a server is not started for the given {@link UUID}. 679 * @throws CanceledException if the pending intent for the server socket was cancelled. 680 */ addIncomingRfcommConnection(BluetoothDevice remoteDevice, UUID uuid)681 public BluetoothSocket addIncomingRfcommConnection(BluetoothDevice remoteDevice, UUID uuid) 682 throws CanceledException { 683 BackgroundRfcommServerEntry entry = backgroundRfcommServers.get(uuid); 684 if (entry == null) { 685 throw new IllegalArgumentException("No RFCOMM server open for UUID: " + uuid); 686 } 687 688 BluetoothSocket socket = shadowOf(entry.serverSocket).deviceConnected(remoteDevice); 689 entry.pendingIntent.send(); 690 691 return socket; 692 } 693 694 /** 695 * Returns an immutable set of {@link UUID}s representing the currently registered RFCOMM servers. 696 */ 697 @SuppressWarnings("JdkImmutableCollections") getRegisteredRfcommServerUuids()698 public Set<UUID> getRegisteredRfcommServerUuids() { 699 return Set.of(backgroundRfcommServers.keySet().toArray(new UUID[0])); 700 } 701 702 private static final class BackgroundRfcommServerEntry { 703 final BluetoothServerSocket serverSocket; 704 final PendingIntent pendingIntent; 705 BackgroundRfcommServerEntry(UUID uuid, PendingIntent pendingIntent)706 BackgroundRfcommServerEntry(UUID uuid, PendingIntent pendingIntent) { 707 this.serverSocket = 708 ShadowBluetoothServerSocket.newInstance( 709 /* type= */ BluetoothSocket.TYPE_RFCOMM, 710 /* auth= */ true, 711 /* encrypt= */ true, 712 new ParcelUuid(uuid)); 713 this.pendingIntent = pendingIntent; 714 } 715 } 716 717 @ForType(BluetoothAdapter.class) 718 interface BluetoothAdapterReflector { 719 720 @Static 721 @Direct getDefaultAdapter()722 BluetoothAdapter getDefaultAdapter(); 723 724 @Direct getProfileProxy( Context context, BluetoothProfile.ServiceListener listener, int profile)725 boolean getProfileProxy( 726 Context context, BluetoothProfile.ServiceListener listener, int profile); 727 728 @Direct closeProfileProxy(int profile, BluetoothProfile proxy)729 void closeProfileProxy(int profile, BluetoothProfile proxy); 730 731 @Direct getRemoteDevice(String address)732 BluetoothDevice getRemoteDevice(String address); 733 734 @Accessor("sAdapter") 735 @Static setAdapter(BluetoothAdapter adapter)736 void setAdapter(BluetoothAdapter adapter); 737 738 @Accessor("mBluetoothLeAdvertiser") 739 @Deprecated setBluetoothLeAdvertiser(BluetoothLeAdvertiser advertiser)740 void setBluetoothLeAdvertiser(BluetoothLeAdvertiser advertiser); 741 742 @Accessor("sBluetoothLeAdvertiser") 743 @Static setSBluetoothLeAdvertiser(BluetoothLeAdvertiser advertiser)744 void setSBluetoothLeAdvertiser(BluetoothLeAdvertiser advertiser); 745 746 @Accessor("sBluetoothLeScanner") 747 @Static setSBluetoothLeScanner(BluetoothLeScanner scanner)748 void setSBluetoothLeScanner(BluetoothLeScanner scanner); 749 } 750 751 // Any BluetoothAdapter calls which need to invoke BluetoothManager methods can delegate those 752 // calls to this class. The default behavior for any methods not defined in this class is a no-op. 753 @SuppressWarnings("unused") 754 private static class BluetoothManagerDelegate { 755 /** 756 * Allows the internal BluetoothProfileConnector associated with a {@link BluetoothProfile} to 757 * automatically invoke the service connected callback. 758 */ bindBluetoothProfileService( int bluetoothProfile, String serviceName, IBluetoothProfileServiceConnection proxy)759 public boolean bindBluetoothProfileService( 760 int bluetoothProfile, String serviceName, IBluetoothProfileServiceConnection proxy) { 761 if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { 762 return false; 763 } 764 try { 765 proxy.onServiceConnected(null, null); 766 } catch (RemoteException e) { 767 return false; 768 } 769 return true; 770 } 771 772 /** 773 * Allows the internal BluetoothProfileConnector associated with a {@link BluetoothProfile} to 774 * automatically invoke the service disconnected callback. 775 */ unbindBluetoothProfileService( int bluetoothProfile, IBluetoothProfileServiceConnection proxy)776 public void unbindBluetoothProfileService( 777 int bluetoothProfile, IBluetoothProfileServiceConnection proxy) { 778 try { 779 proxy.onServiceDisconnected(null); 780 } catch (RemoteException e) { 781 // nothing to do 782 } 783 } 784 } 785 } 786