• 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.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