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