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