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