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