• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.bluetooth.BluetoothDevice.BOND_NONE;
4 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
5 import static android.os.Build.VERSION_CODES.M;
6 import static android.os.Build.VERSION_CODES.O;
7 import static android.os.Build.VERSION_CODES.O_MR1;
8 import static android.os.Build.VERSION_CODES.Q;
9 import static android.os.Build.VERSION_CODES.S;
10 import static org.robolectric.util.reflector.Reflector.reflector;
11 
12 import android.annotation.IntRange;
13 import android.app.ActivityThread;
14 import android.bluetooth.BluetoothClass;
15 import android.bluetooth.BluetoothDevice;
16 import android.bluetooth.BluetoothGatt;
17 import android.bluetooth.BluetoothGattCallback;
18 import android.bluetooth.BluetoothSocket;
19 import android.bluetooth.BluetoothStatusCodes;
20 import android.bluetooth.IBluetooth;
21 import android.content.Context;
22 import android.os.Build.VERSION;
23 import android.os.Handler;
24 import android.os.ParcelUuid;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.UUID;
31 import javax.annotation.Nullable;
32 import org.robolectric.RuntimeEnvironment;
33 import org.robolectric.annotation.Implementation;
34 import org.robolectric.annotation.Implements;
35 import org.robolectric.annotation.RealObject;
36 import org.robolectric.annotation.Resetter;
37 import org.robolectric.shadow.api.Shadow;
38 import org.robolectric.util.ReflectionHelpers;
39 import org.robolectric.util.reflector.Direct;
40 import org.robolectric.util.reflector.ForType;
41 import org.robolectric.util.reflector.Static;
42 
43 /** Shadow for {@link BluetoothDevice}. */
44 @Implements(value = BluetoothDevice.class, looseSignatures = true)
45 public class ShadowBluetoothDevice {
46   /**
47    * Interceptor interface for {@link BluetoothGatt} objects. Tests that require configuration of
48    * their ShadowBluetoothGatt's may inject an interceptor, which will be called with the newly
49    * constructed BluetoothGatt before {@link ShadowBluetoothGatt#connectGatt} returns.
50    */
51   public static interface BluetoothGattConnectionInterceptor {
onNewGattConnection(BluetoothGatt gatt)52     public void onNewGattConnection(BluetoothGatt gatt);
53   }
54 
55   @Deprecated // Prefer {@link android.bluetooth.BluetoothAdapter#getRemoteDevice}
newInstance(String address)56   public static BluetoothDevice newInstance(String address) {
57     return ReflectionHelpers.callConstructor(
58         BluetoothDevice.class, ReflectionHelpers.ClassParameter.from(String.class, address));
59   }
60 
61   @Resetter
reset()62   public static void reset() {
63     bluetoothSocket = null;
64   }
65 
66   private static BluetoothSocket bluetoothSocket = null;
67 
68   @RealObject private BluetoothDevice realBluetoothDevice;
69   private String name;
70   private ParcelUuid[] uuids;
71   private int bondState = BOND_NONE;
72   private boolean createdBond = false;
73   private boolean fetchUuidsWithSdpResult = false;
74   private int fetchUuidsWithSdpCount = 0;
75   private int type = BluetoothDevice.DEVICE_TYPE_UNKNOWN;
76   private final List<BluetoothGatt> bluetoothGatts = new ArrayList<>();
77   private Boolean pairingConfirmation = null;
78   private byte[] pin = null;
79   private String alias;
80   private boolean shouldThrowOnGetAliasName = false;
81   private BluetoothClass bluetoothClass = null;
82   private boolean shouldThrowSecurityExceptions = false;
83   private final Map<Integer, byte[]> metadataMap = new HashMap<>();
84   private int batteryLevel = BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF;
85   private boolean isInSilenceMode = false;
86   private boolean isConnected = false;
87   @Nullable private BluetoothGattConnectionInterceptor bluetoothGattConnectionInterceptor = null;
88 
89   /**
90    * Implements getService() in the same way the original method does, but ignores any Exceptions
91    * from invoking {@link android.bluetooth.BluetoothAdapter#getBluetoothService}.
92    */
93   @Implementation
getService()94   protected static IBluetooth getService() {
95     // Attempt to call the underlying getService method, but ignore any Exceptions. This allows us
96     // to easily create BluetoothDevices for testing purposes without having any actual Bluetooth
97     // capability.
98     try {
99       return reflector(BluetoothDeviceReflector.class).getService();
100     } catch (Exception e) {
101       // No-op.
102     }
103     return null;
104   }
105 
setName(String name)106   public void setName(String name) {
107     this.name = name;
108   }
109 
110   /**
111    * Sets the alias name of the device.
112    *
113    * <p>Alias is the locally modified name of a remote device.
114    *
115    * <p>Alias Name is not part of the supported SDK, and accessed via reflection.
116    *
117    * @param alias alias name.
118    */
119   @Implementation
setAlias(Object alias)120   public Object setAlias(Object alias) {
121     this.alias = (String) alias;
122     if (RuntimeEnvironment.getApiLevel() >= S) {
123       return BluetoothStatusCodes.SUCCESS;
124     } else {
125       return true;
126     }
127   }
128 
129   /**
130    * Sets if a runtime exception is thrown when the alias name of the device is accessed.
131    *
132    * <p>Intended to replicate what may happen if the unsupported SDK is changed.
133    *
134    * <p>Alias is the locally modified name of a remote device.
135    *
136    * <p>Alias Name is not part of the supported SDK, and accessed via reflection.
137    *
138    * @param shouldThrow if getAliasName() should throw when called.
139    */
setThrowOnGetAliasName(boolean shouldThrow)140   public void setThrowOnGetAliasName(boolean shouldThrow) {
141     shouldThrowOnGetAliasName = shouldThrow;
142   }
143 
144   /**
145    * Sets if a runtime exception is thrown when bluetooth methods with BLUETOOTH_CONNECT permission
146    * pre-requisites are accessed.
147    *
148    * <p>Intended to replicate what may happen if user has not enabled nearby device permissions.
149    *
150    * @param shouldThrow if methods should throw SecurityExceptions without enabled permissions when
151    *     called.
152    */
setShouldThrowSecurityExceptions(boolean shouldThrow)153   public void setShouldThrowSecurityExceptions(boolean shouldThrow) {
154     shouldThrowSecurityExceptions = shouldThrow;
155   }
156 
157   @Implementation
getName()158   protected String getName() {
159     checkForBluetoothConnectPermission();
160     return name;
161   }
162 
163   @Implementation
getAlias()164   protected String getAlias() {
165     checkForBluetoothConnectPermission();
166     return alias;
167   }
168 
169   @Implementation(maxSdk = Q)
getAliasName()170   protected String getAliasName() throws ReflectiveOperationException {
171     // Mimicking if the officially supported function is changed.
172     if (shouldThrowOnGetAliasName) {
173       throw new ReflectiveOperationException("Exception on getAliasName");
174     }
175 
176     // Matches actual implementation.
177     String name = getAlias();
178     return name != null ? name : getName();
179   }
180 
181   /** Sets the return value for {@link BluetoothDevice#getType}. */
setType(int type)182   public void setType(int type) {
183     this.type = type;
184   }
185 
186   /**
187    * Overrides behavior of {@link BluetoothDevice#getType} to return pre-set result.
188    *
189    * @return Value set by calling {@link ShadowBluetoothDevice#setType}. If setType has not
190    *     previously been called, will return BluetoothDevice.DEVICE_TYPE_UNKNOWN.
191    */
192   @Implementation
getType()193   protected int getType() {
194     checkForBluetoothConnectPermission();
195     return type;
196   }
197 
198   /** Sets the return value for {@link BluetoothDevice#getUuids}. */
setUuids(ParcelUuid[] uuids)199   public void setUuids(ParcelUuid[] uuids) {
200     this.uuids = uuids;
201   }
202 
203   /**
204    * Overrides behavior of {@link BluetoothDevice#getUuids} to return pre-set result.
205    *
206    * @return Value set by calling {@link ShadowBluetoothDevice#setUuids}. If setUuids has not
207    *     previously been called, will return null.
208    */
209   @Implementation
getUuids()210   protected ParcelUuid[] getUuids() {
211     checkForBluetoothConnectPermission();
212     return uuids;
213   }
214 
215   /** Sets value of bond state for {@link BluetoothDevice#getBondState}. */
setBondState(int bondState)216   public void setBondState(int bondState) {
217     this.bondState = bondState;
218   }
219 
220   /**
221    * Overrides behavior of {@link BluetoothDevice#getBondState} to return pre-set result.
222    *
223    * @returns Value set by calling {@link ShadowBluetoothDevice#setBondState}. If setBondState has
224    *     not previously been called, will return {@link BluetoothDevice#BOND_NONE} to indicate the
225    *     device is not bonded.
226    */
227   @Implementation
getBondState()228   protected int getBondState() {
229     checkForBluetoothConnectPermission();
230     return bondState;
231   }
232 
233   /** Sets whether this device has been bonded with. */
setCreatedBond(boolean createdBond)234   public void setCreatedBond(boolean createdBond) {
235     this.createdBond = createdBond;
236   }
237 
238   /** Returns whether this device has been bonded with. */
239   @Implementation
createBond()240   protected boolean createBond() {
241     checkForBluetoothConnectPermission();
242     return createdBond;
243   }
244 
245   @Implementation(minSdk = Q)
createInsecureL2capChannel(int psm)246   protected BluetoothSocket createInsecureL2capChannel(int psm) throws IOException {
247     checkForBluetoothConnectPermission();
248     return reflector(BluetoothDeviceReflector.class, realBluetoothDevice)
249         .createInsecureL2capChannel(psm);
250   }
251 
252   @Implementation(minSdk = Q)
createL2capChannel(int psm)253   protected BluetoothSocket createL2capChannel(int psm) throws IOException {
254     checkForBluetoothConnectPermission();
255     return reflector(BluetoothDeviceReflector.class, realBluetoothDevice).createL2capChannel(psm);
256   }
257 
258   @Implementation
removeBond()259   protected boolean removeBond() {
260     checkForBluetoothConnectPermission();
261     boolean result = createdBond;
262     createdBond = false;
263     return result;
264   }
265 
266   @Implementation
setPin(byte[] pin)267   protected boolean setPin(byte[] pin) {
268     checkForBluetoothConnectPermission();
269     this.pin = pin;
270     return true;
271   }
272 
273   /**
274    * Get the PIN previously set with a call to {@link BluetoothDevice#setPin(byte[])}, or null if no
275    * PIN has been set.
276    */
getPin()277   public byte[] getPin() {
278     return pin;
279   }
280 
281   @Implementation
setPairingConfirmation(boolean confirm)282   public boolean setPairingConfirmation(boolean confirm) {
283     checkForBluetoothConnectPermission();
284     this.pairingConfirmation = confirm;
285     return true;
286   }
287 
288   /**
289    * Get the confirmation value previously set with a call to {@link
290    * BluetoothDevice#setPairingConfirmation(boolean)}, or null if no value is set.
291    */
getPairingConfirmation()292   public Boolean getPairingConfirmation() {
293     return pairingConfirmation;
294   }
295 
296   @Implementation
createRfcommSocketToServiceRecord(UUID uuid)297   protected BluetoothSocket createRfcommSocketToServiceRecord(UUID uuid) throws IOException {
298     checkForBluetoothConnectPermission();
299     synchronized (ShadowBluetoothDevice.class) {
300       if (bluetoothSocket == null) {
301         bluetoothSocket = Shadow.newInstanceOf(BluetoothSocket.class);
302       }
303     }
304     return bluetoothSocket;
305   }
306 
307   /** Sets value of the return result for {@link BluetoothDevice#fetchUuidsWithSdp}. */
setFetchUuidsWithSdpResult(boolean fetchUuidsWithSdpResult)308   public void setFetchUuidsWithSdpResult(boolean fetchUuidsWithSdpResult) {
309     this.fetchUuidsWithSdpResult = fetchUuidsWithSdpResult;
310   }
311 
312   /**
313    * Overrides behavior of {@link BluetoothDevice#fetchUuidsWithSdp}. This method updates the
314    * counter which counts the number of invocations of this method.
315    *
316    * @returns Value set by calling {@link ShadowBluetoothDevice#setFetchUuidsWithSdpResult}. If not
317    *     previously set, will return false by default.
318    */
319   @Implementation
fetchUuidsWithSdp()320   protected boolean fetchUuidsWithSdp() {
321     checkForBluetoothConnectPermission();
322     fetchUuidsWithSdpCount++;
323     return fetchUuidsWithSdpResult;
324   }
325 
326   /** Returns the number of times fetchUuidsWithSdp has been called. */
getFetchUuidsWithSdpCount()327   public int getFetchUuidsWithSdpCount() {
328     return fetchUuidsWithSdpCount;
329   }
330 
331   @Implementation
connectGatt( Context context, boolean autoConnect, BluetoothGattCallback callback)332   protected BluetoothGatt connectGatt(
333       Context context, boolean autoConnect, BluetoothGattCallback callback) {
334     checkForBluetoothConnectPermission();
335     return connectGatt(callback);
336   }
337 
338   @Implementation(minSdk = M)
connectGatt( Context context, boolean autoConnect, BluetoothGattCallback callback, int transport)339   protected BluetoothGatt connectGatt(
340       Context context, boolean autoConnect, BluetoothGattCallback callback, int transport) {
341     checkForBluetoothConnectPermission();
342     return connectGatt(callback);
343   }
344 
345   @Implementation(minSdk = O)
connectGatt( Context context, boolean autoConnect, BluetoothGattCallback callback, int transport, int phy, Handler handler)346   protected BluetoothGatt connectGatt(
347       Context context,
348       boolean autoConnect,
349       BluetoothGattCallback callback,
350       int transport,
351       int phy,
352       Handler handler) {
353     checkForBluetoothConnectPermission();
354     return connectGatt(callback);
355   }
356 
connectGatt(BluetoothGattCallback callback)357   private BluetoothGatt connectGatt(BluetoothGattCallback callback) {
358     BluetoothGatt bluetoothGatt = ShadowBluetoothGatt.newInstance(realBluetoothDevice);
359     bluetoothGatts.add(bluetoothGatt);
360     ShadowBluetoothGatt shadowBluetoothGatt = Shadow.extract(bluetoothGatt);
361     shadowBluetoothGatt.setGattCallback(callback);
362 
363     if (bluetoothGattConnectionInterceptor != null) {
364       bluetoothGattConnectionInterceptor.onNewGattConnection(bluetoothGatt);
365     }
366 
367     return bluetoothGatt;
368   }
369 
370   /**
371    * Returns all {@link BluetoothGatt} objects created by calling {@link
372    * ShadowBluetoothDevice#connectGatt}.
373    */
getBluetoothGatts()374   public List<BluetoothGatt> getBluetoothGatts() {
375     return bluetoothGatts;
376   }
377 
378   /**
379    * Causes {@link BluetoothGattCallback#onConnectionStateChange to be called for every GATT client.
380    * @param status Status of the GATT operation
381    * @param newState The new state of the GATT profile
382    */
simulateGattConnectionChange(int status, int newState)383   public void simulateGattConnectionChange(int status, int newState) {
384     for (BluetoothGatt bluetoothGatt : bluetoothGatts) {
385       ShadowBluetoothGatt shadowBluetoothGatt = Shadow.extract(bluetoothGatt);
386       BluetoothGattCallback gattCallback = shadowBluetoothGatt.getGattCallback();
387       gattCallback.onConnectionStateChange(bluetoothGatt, status, newState);
388     }
389   }
390 
391   /**
392    * Overrides behavior of {@link BluetoothDevice#getBluetoothClass} to return pre-set result.
393    *
394    * @return Value set by calling {@link ShadowBluetoothDevice#setBluetoothClass}. If setType has
395    *     not previously been called, will return null.
396    */
397   @Implementation
getBluetoothClass()398   public BluetoothClass getBluetoothClass() {
399     checkForBluetoothConnectPermission();
400     return bluetoothClass;
401   }
402 
403   /** Sets the return value for {@link BluetoothDevice#getBluetoothClass}. */
setBluetoothClass(BluetoothClass bluetoothClass)404   public void setBluetoothClass(BluetoothClass bluetoothClass) {
405     this.bluetoothClass = bluetoothClass;
406   }
407 
408   @Implementation(minSdk = Q)
setMetadata(int key, byte[] value)409   protected boolean setMetadata(int key, byte[] value) {
410     checkForBluetoothConnectPermission();
411     metadataMap.put(key, value);
412     return true;
413   }
414 
415   @Implementation(minSdk = Q)
getMetadata(int key)416   protected byte[] getMetadata(int key) {
417     checkForBluetoothConnectPermission();
418     return metadataMap.get(key);
419   }
420 
setBatteryLevel(@ntRangefrom = -100, to = 100) int batteryLevel)421   public void setBatteryLevel(@IntRange(from = -100, to = 100) int batteryLevel) {
422     this.batteryLevel = batteryLevel;
423   }
424 
425   @Implementation(minSdk = O_MR1)
getBatteryLevel()426   protected int getBatteryLevel() {
427     checkForBluetoothConnectPermission();
428     return batteryLevel;
429   }
430 
431   @Implementation(minSdk = Q)
setSilenceMode(boolean isInSilenceMode)432   public boolean setSilenceMode(boolean isInSilenceMode) {
433     checkForBluetoothConnectPermission();
434     this.isInSilenceMode = isInSilenceMode;
435     return true;
436   }
437 
438   @Implementation
isConnected()439   protected boolean isConnected() {
440     return isConnected;
441   }
442 
setConnected(boolean isConnected)443   public void setConnected(boolean isConnected) {
444     this.isConnected = isConnected;
445   }
446 
447   @Implementation(minSdk = Q)
isInSilenceMode()448   protected boolean isInSilenceMode() {
449     checkForBluetoothConnectPermission();
450     return isInSilenceMode;
451   }
452 
453   /**
454    * Allows tests to intercept the {@link BluetoothDevice.connectGatt} method and set state on both
455    * BluetoothDevice and BluetoothGatt objects. This is useful for e2e testing situations where the
456    * fine-grained execution of Bluetooth connection logic is onerous.
457    */
setGattConnectionInterceptor(BluetoothGattConnectionInterceptor interceptor)458   public void setGattConnectionInterceptor(BluetoothGattConnectionInterceptor interceptor) {
459     bluetoothGattConnectionInterceptor = interceptor;
460   }
461 
462   @ForType(BluetoothDevice.class)
463   interface BluetoothDeviceReflector {
464 
465     @Static
466     @Direct
getService()467     IBluetooth getService();
468 
469     @Direct
createInsecureL2capChannel(int psm)470     BluetoothSocket createInsecureL2capChannel(int psm);
471 
472     @Direct
createL2capChannel(int psm)473     BluetoothSocket createL2capChannel(int psm);
474   }
475 
getShadowInstrumentation()476   static ShadowInstrumentation getShadowInstrumentation() {
477     ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
478     return Shadow.extract(activityThread.getInstrumentation());
479   }
480 
checkForBluetoothConnectPermission()481   private void checkForBluetoothConnectPermission() {
482     if (shouldThrowSecurityExceptions
483         && VERSION.SDK_INT >= S
484         && !checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT)) {
485       throw new SecurityException("Bluetooth connect permission required.");
486     }
487   }
488 
checkPermission(String permission)489   static boolean checkPermission(String permission) {
490     return getShadowInstrumentation()
491             .checkPermission(permission, android.os.Process.myPid(), android.os.Process.myUid())
492         == PERMISSION_GRANTED;
493   }
494 }
495