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