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