
org.robolectric.shadows.ShadowBluetoothDevice Maven / Gradle / Ivy
Show all versions of shadows-framework Show documentation
package org.robolectric.shadows;
import static android.bluetooth.BluetoothDevice.BOND_BONDING;
import static android.bluetooth.BluetoothDevice.BOND_NONE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.O_MR1;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.annotation.IntRange;
import android.app.ActivityThread;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothSocket;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.IBluetooth;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Handler;
import android.os.ParcelUuid;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Static;
/** Shadow for {@link BluetoothDevice}. */
@Implements(value = BluetoothDevice.class)
public class ShadowBluetoothDevice {
/**
* Interceptor interface for {@link BluetoothGatt} objects. Tests that require configuration of
* their ShadowBluetoothGatt's may inject an interceptor, which will be called with the newly
* constructed BluetoothGatt before {@link ShadowBluetoothGatt#connectGatt} returns.
*/
public static interface BluetoothGattConnectionInterceptor {
public void onNewGattConnection(BluetoothGatt gatt);
}
@Deprecated // Prefer {@link android.bluetooth.BluetoothAdapter#getRemoteDevice}
public static BluetoothDevice newInstance(String address) {
return ReflectionHelpers.callConstructor(
BluetoothDevice.class, ReflectionHelpers.ClassParameter.from(String.class, address));
}
@Resetter
public static void reset() {
bluetoothSocket = null;
}
private static BluetoothSocket bluetoothSocket = null;
@RealObject private BluetoothDevice realBluetoothDevice;
private String name;
private ParcelUuid[] uuids;
private int bondState = BOND_NONE;
private boolean createdBond = false;
private boolean fetchUuidsWithSdpResult = false;
private int fetchUuidsWithSdpCount = 0;
private int type = BluetoothDevice.DEVICE_TYPE_UNKNOWN;
private final List bluetoothGatts = new ArrayList<>();
private Boolean pairingConfirmation = null;
private byte[] pin = null;
private String alias;
private boolean shouldThrowOnGetAliasName = false;
private BluetoothClass bluetoothClass = null;
private boolean shouldThrowSecurityExceptions = false;
private final Map metadataMap = new HashMap<>();
private int batteryLevel = BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF;
private boolean isInSilenceMode = false;
private boolean isConnected = false;
@Nullable private BluetoothGattConnectionInterceptor bluetoothGattConnectionInterceptor = null;
private Map connectionHandlesByTransportType = new HashMap<>();
/**
* Implements getService() in the same way the original method does, but ignores any Exceptions
* from invoking {@link android.bluetooth.BluetoothAdapter#getBluetoothService}.
*/
@Implementation
protected static IBluetooth getService() {
// Attempt to call the underlying getService method, but ignore any Exceptions. This allows us
// to easily create BluetoothDevices for testing purposes without having any actual Bluetooth
// capability.
try {
return reflector(BluetoothDeviceReflector.class).getService();
} catch (Exception e) {
// No-op.
}
return null;
}
public void setName(String name) {
this.name = name;
}
/**
* Set the alias for bluetooth device.
*
* @param alias The alias name that set to bluetooth device.
* @return If API is larger than or equals to S, it returns [BluetoothStatusCodes] code, otherwise
* it returns boolean.
*/
public Object setAlias(String alias) {
if (RuntimeEnvironment.getApiLevel() >= S) {
return setAliasS(alias);
} else {
return setAliasBeforeS(alias);
}
}
/**
* Sets the alias name of the device.
*
* Alias is the locally modified name of a remote device.
*
*
Alias Name is not part of the supported SDK, and accessed via reflection.
*
* @param alias alias name.
*/
@Implementation(maxSdk = R, methodName = "setAlias")
protected boolean setAliasBeforeS(String alias) {
this.alias = alias;
return true;
}
/**
* Sets the alias name of the device for API >= 31.
*
*
Alias is the locally modified name of a remote device.
*
*
Alias Name is not part of the supported SDK, and accessed via reflection.
*
* @param alias alias name.
*/
@Implementation(minSdk = S, methodName = "setAlias")
protected int setAliasS(String alias) {
this.alias = alias;
return BluetoothStatusCodes.SUCCESS;
}
/**
* Sets if a runtime exception is thrown when the alias name of the device is accessed.
*
*
Intended to replicate what may happen if the unsupported SDK is changed.
*
*
Alias is the locally modified name of a remote device.
*
*
Alias Name is not part of the supported SDK, and accessed via reflection.
*
* @param shouldThrow if getAliasName() should throw when called.
*/
public void setThrowOnGetAliasName(boolean shouldThrow) {
shouldThrowOnGetAliasName = shouldThrow;
}
/**
* Sets if a runtime exception is thrown when bluetooth methods with BLUETOOTH_CONNECT permission
* pre-requisites are accessed.
*
*
Intended to replicate what may happen if user has not enabled nearby device permissions.
*
* @param shouldThrow if methods should throw SecurityExceptions without enabled permissions when
* called.
*/
public void setShouldThrowSecurityExceptions(boolean shouldThrow) {
shouldThrowSecurityExceptions = shouldThrow;
}
@Implementation
protected String getName() {
checkForBluetoothConnectPermission();
return name;
}
@Implementation
protected String getAlias() {
checkForBluetoothConnectPermission();
return alias;
}
@Implementation(maxSdk = Q)
protected String getAliasName() throws ReflectiveOperationException {
// Mimicking if the officially supported function is changed.
if (shouldThrowOnGetAliasName) {
throw new ReflectiveOperationException("Exception on getAliasName");
}
// Matches actual implementation.
String name = getAlias();
return name != null ? name : getName();
}
/** Sets the return value for {@link BluetoothDevice#getType}. */
public void setType(int type) {
this.type = type;
}
/**
* Overrides behavior of {@link BluetoothDevice#getType} to return pre-set result.
*
* @return Value set by calling {@link ShadowBluetoothDevice#setType}. If setType has not
* previously been called, will return BluetoothDevice.DEVICE_TYPE_UNKNOWN.
*/
@Implementation
protected int getType() {
checkForBluetoothConnectPermission();
return type;
}
/** Sets the return value for {@link BluetoothDevice#getUuids}. */
public void setUuids(ParcelUuid[] uuids) {
this.uuids = uuids;
}
/**
* Overrides behavior of {@link BluetoothDevice#getUuids} to return pre-set result.
*
* @return Value set by calling {@link ShadowBluetoothDevice#setUuids}. If setUuids has not
* previously been called, will return null.
*/
@Implementation
protected ParcelUuid[] getUuids() {
checkForBluetoothConnectPermission();
return uuids;
}
/** Sets value of bond state for {@link BluetoothDevice#getBondState}. */
public void setBondState(int bondState) {
this.bondState = bondState;
}
@Implementation
protected boolean cancelBondProcess() {
if (bondState == BOND_BONDING) {
setBondState(BOND_NONE);
return true;
}
return false;
}
/**
* Overrides behavior of {@link BluetoothDevice#getBondState} to return pre-set result.
*
* @return Value set by calling {@link ShadowBluetoothDevice#setBondState}. If setBondState has
* not previously been called, will return {@link BluetoothDevice#BOND_NONE} to indicate the
* device is not bonded.
*/
@Implementation
protected int getBondState() {
checkForBluetoothConnectPermission();
return bondState;
}
/** Sets whether this device has been bonded with. */
public void setCreatedBond(boolean createdBond) {
this.createdBond = createdBond;
}
/** Returns whether this device has been bonded with. */
@Implementation
protected boolean createBond() {
checkForBluetoothConnectPermission();
return createdBond;
}
@Implementation(minSdk = Q)
protected BluetoothSocket createInsecureL2capChannel(int psm) throws IOException {
checkForBluetoothConnectPermission();
return reflector(BluetoothDeviceReflector.class, realBluetoothDevice)
.createInsecureL2capChannel(psm);
}
@Implementation(minSdk = Q)
protected BluetoothSocket createL2capChannel(int psm) throws IOException {
checkForBluetoothConnectPermission();
return reflector(BluetoothDeviceReflector.class, realBluetoothDevice).createL2capChannel(psm);
}
@Implementation
protected boolean removeBond() {
checkForBluetoothConnectPermission();
boolean result = createdBond;
createdBond = false;
return result;
}
@Implementation
protected boolean setPin(byte[] pin) {
checkForBluetoothConnectPermission();
this.pin = pin;
return true;
}
/**
* Get the PIN previously set with a call to {@link BluetoothDevice#setPin(byte[])}, or null if no
* PIN has been set.
*/
public byte[] getPin() {
return pin;
}
@Implementation
public boolean setPairingConfirmation(boolean confirm) {
checkForBluetoothConnectPermission();
this.pairingConfirmation = confirm;
return true;
}
/**
* Get the confirmation value previously set with a call to {@link
* BluetoothDevice#setPairingConfirmation(boolean)}, or null if no value is set.
*/
public Boolean getPairingConfirmation() {
return pairingConfirmation;
}
@Implementation
protected BluetoothSocket createRfcommSocketToServiceRecord(UUID uuid) throws IOException {
checkForBluetoothConnectPermission();
synchronized (ShadowBluetoothDevice.class) {
if (bluetoothSocket == null) {
bluetoothSocket = Shadow.newInstanceOf(BluetoothSocket.class);
}
}
return bluetoothSocket;
}
/** Sets value of the return result for {@link BluetoothDevice#fetchUuidsWithSdp}. */
public void setFetchUuidsWithSdpResult(boolean fetchUuidsWithSdpResult) {
this.fetchUuidsWithSdpResult = fetchUuidsWithSdpResult;
}
/**
* Overrides behavior of {@link BluetoothDevice#fetchUuidsWithSdp}. This method updates the
* counter which counts the number of invocations of this method.
*
* @return Value set by calling {@link ShadowBluetoothDevice#setFetchUuidsWithSdpResult}. If not
* previously set, will return false by default.
*/
@Implementation
protected boolean fetchUuidsWithSdp() {
checkForBluetoothConnectPermission();
fetchUuidsWithSdpCount++;
return fetchUuidsWithSdpResult;
}
/** Returns the number of times fetchUuidsWithSdp has been called. */
public int getFetchUuidsWithSdpCount() {
return fetchUuidsWithSdpCount;
}
@Implementation
protected BluetoothGatt connectGatt(
Context context, boolean autoConnect, BluetoothGattCallback callback) {
checkForBluetoothConnectPermission();
return connectGatt(callback);
}
@Implementation(minSdk = M)
protected BluetoothGatt connectGatt(
Context context, boolean autoConnect, BluetoothGattCallback callback, int transport) {
checkForBluetoothConnectPermission();
return connectGatt(callback);
}
@Implementation(minSdk = O)
protected BluetoothGatt connectGatt(
Context context,
boolean autoConnect,
BluetoothGattCallback callback,
int transport,
int phy,
Handler handler) {
checkForBluetoothConnectPermission();
return connectGatt(callback);
}
private BluetoothGatt connectGatt(BluetoothGattCallback callback) {
BluetoothGatt bluetoothGatt = ShadowBluetoothGatt.newInstance(realBluetoothDevice);
bluetoothGatts.add(bluetoothGatt);
ShadowBluetoothGatt shadowBluetoothGatt = Shadow.extract(bluetoothGatt);
shadowBluetoothGatt.setGattCallback(callback);
if (bluetoothGattConnectionInterceptor != null) {
bluetoothGattConnectionInterceptor.onNewGattConnection(bluetoothGatt);
}
return bluetoothGatt;
}
/**
* Returns all {@link BluetoothGatt} objects created by calling {@link
* ShadowBluetoothDevice#connectGatt}.
*/
public List getBluetoothGatts() {
return bluetoothGatts;
}
/**
* Causes {@link BluetoothGattCallback#onConnectionStateChange to be called for every GATT client.
* @param status Status of the GATT operation
* @param newState The new state of the GATT profile
*/
public void simulateGattConnectionChange(int status, int newState) {
for (BluetoothGatt bluetoothGatt : bluetoothGatts) {
ShadowBluetoothGatt shadowBluetoothGatt = Shadow.extract(bluetoothGatt);
BluetoothGattCallback gattCallback = shadowBluetoothGatt.getGattCallback();
gattCallback.onConnectionStateChange(bluetoothGatt, status, newState);
}
}
/**
* Overrides behavior of {@link BluetoothDevice#getBluetoothClass} to return pre-set result.
*
* @return Value set by calling {@link ShadowBluetoothDevice#setBluetoothClass}. If setType has
* not previously been called, will return null.
*/
@Implementation
public BluetoothClass getBluetoothClass() {
checkForBluetoothConnectPermission();
return bluetoothClass;
}
/** Sets the return value for {@link BluetoothDevice#getBluetoothClass}. */
public void setBluetoothClass(BluetoothClass bluetoothClass) {
this.bluetoothClass = bluetoothClass;
}
@Implementation(minSdk = Q)
protected boolean setMetadata(int key, byte[] value) {
checkForBluetoothConnectPermission();
metadataMap.put(key, value);
return true;
}
@Implementation(minSdk = Q)
protected byte[] getMetadata(int key) {
checkForBluetoothConnectPermission();
return metadataMap.get(key);
}
public void setBatteryLevel(@IntRange(from = -100, to = 100) int batteryLevel) {
this.batteryLevel = batteryLevel;
}
@Implementation(minSdk = O_MR1)
protected int getBatteryLevel() {
checkForBluetoothConnectPermission();
return batteryLevel;
}
@Implementation(minSdk = Q)
public boolean setSilenceMode(boolean isInSilenceMode) {
checkForBluetoothConnectPermission();
this.isInSilenceMode = isInSilenceMode;
return true;
}
@Implementation
protected boolean isConnected() {
return isConnected;
}
public void setConnected(boolean isConnected) {
this.isConnected = isConnected;
}
@Implementation(minSdk = UPSIDE_DOWN_CAKE)
protected int getConnectionHandle(int transport) {
if (!connectionHandlesByTransportType.containsKey(transport)) {
return 0;
}
return connectionHandlesByTransportType.get(transport);
}
public void setConnectionHandle(int transport, int connectionHandle) {
connectionHandlesByTransportType.put(transport, connectionHandle);
}
@Implementation(minSdk = Q)
protected boolean isInSilenceMode() {
checkForBluetoothConnectPermission();
return isInSilenceMode;
}
/**
* Allows tests to intercept the {@link BluetoothDevice.connectGatt} method and set state on both
* BluetoothDevice and BluetoothGatt objects. This is useful for e2e testing situations where the
* fine-grained execution of Bluetooth connection logic is onerous.
*/
public void setGattConnectionInterceptor(BluetoothGattConnectionInterceptor interceptor) {
bluetoothGattConnectionInterceptor = interceptor;
}
@ForType(BluetoothDevice.class)
interface BluetoothDeviceReflector {
@Static
@Direct
IBluetooth getService();
@Direct
BluetoothSocket createInsecureL2capChannel(int psm);
@Direct
BluetoothSocket createL2capChannel(int psm);
}
static ShadowInstrumentation getShadowInstrumentation() {
ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
return Shadow.extract(activityThread.getInstrumentation());
}
private void checkForBluetoothConnectPermission() {
if (shouldThrowSecurityExceptions
&& VERSION.SDK_INT >= S
&& !checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT)) {
throw new SecurityException("Bluetooth connect permission required.");
}
}
static boolean checkPermission(String permission) {
return getShadowInstrumentation()
.checkPermission(permission, android.os.Process.myPid(), android.os.Process.myUid())
== PERMISSION_GRANTED;
}
}