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