All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowBluetoothGatt Maven / Gradle / Ivy

package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.O_MR1;
import static android.os.Build.VERSION_CODES.R;

import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.ReflectorObject;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.PerfStatsCollector;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;

/** Shadow implementation of {@link BluetoothGatt}. */
@Implements(value = BluetoothGatt.class, minSdk = JELLY_BEAN_MR2)
public class ShadowBluetoothGatt {

  private static final String NULL_CALLBACK_MSG = "BluetoothGattCallback can not be null.";
  
  private BluetoothGattCallback bluetoothGattCallback;
  private int connectionPriority = BluetoothGatt.CONNECTION_PRIORITY_BALANCED;
  private boolean isConnected = false;
  private boolean isClosed = false;
  private byte[] writtenBytes;
  private byte[] readBytes;
  private final Set discoverableServices = new HashSet<>();
  private final ArrayList services = new ArrayList<>();
      
  @RealObject private BluetoothGatt realBluetoothGatt;
  @ReflectorObject protected BluetoothGattReflector bluetoothGattReflector;

  @SuppressLint("PrivateApi")
  @SuppressWarnings("unchecked")
  public static BluetoothGatt newInstance(BluetoothDevice device) {
    try {
      Class iBluetoothGattClass =
          Shadow.class.getClassLoader().loadClass("android.bluetooth.IBluetoothGatt");

      BluetoothGatt bluetoothGatt;
      int apiLevel = RuntimeEnvironment.getApiLevel();
      if (apiLevel > R) {
        bluetoothGatt =
            Shadow.newInstance(
                BluetoothGatt.class,
                new Class[] {
                  iBluetoothGattClass,
                  BluetoothDevice.class,
                  Integer.TYPE,
                  Boolean.TYPE,
                  Integer.TYPE,
                  Class.forName("android.content.AttributionSource"),
                },
                new Object[] {null, device, 0, false, 0, null});
      } else if (apiLevel >= O_MR1) {
        bluetoothGatt =
            Shadow.newInstance(
                BluetoothGatt.class,
                new Class[] {
                  iBluetoothGattClass,
                  BluetoothDevice.class,
                  Integer.TYPE,
                  Boolean.TYPE,
                  Integer.TYPE
                },
                new Object[] {null, device, 0, false, 0});
      } else if (apiLevel >= O) {
        bluetoothGatt =
            Shadow.newInstance(
                BluetoothGatt.class,
                new Class[] {
                  iBluetoothGattClass, BluetoothDevice.class, Integer.TYPE, Integer.TYPE
                },
                new Object[] {null, device, 0, 0});
      } else if (apiLevel >= LOLLIPOP) {
        bluetoothGatt =
            Shadow.newInstance(
                BluetoothGatt.class,
                new Class[] {
                  Context.class, iBluetoothGattClass, BluetoothDevice.class, Integer.TYPE
                },
                new Object[] {RuntimeEnvironment.getApplication(), null, device, 0});
      } else {
        bluetoothGatt =
            Shadow.newInstance(
                BluetoothGatt.class,
                new Class[] {Context.class, iBluetoothGattClass, BluetoothDevice.class},
                new Object[] {RuntimeEnvironment.getApplication(), null, device});
      }

      PerfStatsCollector.getInstance().incrementCount("constructShadowBluetoothGatt");
      return bluetoothGatt;
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Connect to a remote device, and performs a {@link
   * BluetoothGattCallback#onConnectionStateChange} if a {@link BluetoothGattCallback} has been set
   * by {@link ShadowBluetoothGatt#setGattCallback}
   *
   * @return true, if a {@link BluetoothGattCallback} has been set by {@link
   *     ShadowBluetoothGatt#setGattCallback}
   */
  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected boolean connect() {
    if (this.getGattCallback() != null) {
      this.isConnected = true;
      this.getGattCallback()
          .onConnectionStateChange(
              this.realBluetoothGatt, BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED);
      return true;
    }
    return false;
  }

  /**
   * Disconnects an established connection, or cancels a connection attempt currently in progress.
   */
  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected void disconnect() {
    bluetoothGattReflector.disconnect();
    if (this.isCallbackAppropriate()) {
      this.getGattCallback()
          .onConnectionStateChange(
              this.realBluetoothGatt,
              BluetoothGatt.GATT_SUCCESS,
              BluetoothProfile.STATE_DISCONNECTED);
    }
    this.isConnected = false;
  }

  /** Close this Bluetooth GATT client. */
  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected void close() {
    bluetoothGattReflector.close();
    this.isClosed = true;
    this.isConnected = false;
  }

  /**
   * Request a connection parameter update.
   *
   * @param priority Request a specific connection priority. Must be one of {@link
   *     BluetoothGatt#CONNECTION_PRIORITY_BALANCED}, {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH}
   *     or {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}.
   * @return true if operation is successful.
   * @throws IllegalArgumentException If the parameters are outside of their specified range.
   */
  @Implementation(minSdk = O)
  protected boolean requestConnectionPriority(int priority) {
    if (priority == BluetoothGatt.CONNECTION_PRIORITY_HIGH
        || priority == BluetoothGatt.CONNECTION_PRIORITY_BALANCED
        || priority == BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER) {
      this.connectionPriority = priority;
      return true;
    }
    throw new IllegalArgumentException("connection priority not within valid range");
  }

  /**
   * Overrides {@link BluetoothGatt#discoverServices} to always return false unless there are
   * discoverable services made available by {@link ShadowBluetoothGatt#addDiscoverableService}
   *
   * @return true if discoverable service is available and callback response is possible
   */
  @Implementation(minSdk = O)
  protected boolean discoverServices() {
    this.services.clear();
    if (!this.discoverableServices.isEmpty()) {
      this.services.addAll(this.discoverableServices);

      if (this.getGattCallback() != null) {
        this.getGattCallback()
            .onServicesDiscovered(this.realBluetoothGatt, BluetoothGatt.GATT_SUCCESS);
        return true;
      }
    }
    return false;
  }

  /**
   * Overrides {@link BluetoothGatt#getServices} to always return a list of services discovered.
   *
   * @return list of services that have been discovered through {@link
   *     ShadowBluetoothGatt#discoverServices}, empty if none.
   */
  @Implementation(minSdk = O)
  protected List getServices() {
    return new ArrayList<>(this.services);
  }

  /**
   * Reads bytes from incoming characteristic if properties are valid and callback is set. Callback
   * responds with {@link BluetoothGattCallback#onCharacteristicWrite} and returns true when
   * successful.
   *
   * @param characteristic Characteristic to read
   * @return true, if the read operation was initiated successfully
   * @throws IllegalStateException if a {@link BluetoothGattCallback} has not been set by {@link
   *     ShadowBluetoothGatt#setGattCallback}
   */
  public boolean writeIncomingCharacteristic(BluetoothGattCharacteristic characteristic) {
    if (this.getGattCallback() == null) {
      throw new IllegalStateException(NULL_CALLBACK_MSG);
    }
    if (characteristic.getService() == null
        || ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) == 0
            && (characteristic.getProperties()
                    & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)
                == 0)) {
      return false;
    }
    this.writtenBytes = characteristic.getValue();
    this.bluetoothGattCallback.onCharacteristicWrite(
        this.realBluetoothGatt, characteristic, BluetoothGatt.GATT_SUCCESS);
    return true;
  }

  /**
   * Writes bytes from incoming characteristic if properties are valid and callback is set. Callback
   * responds with BluetoothGattCallback#onCharacteristicRead and returns true when successful.
   *
   * @param characteristic Characteristic to read
   * @return true, if the read operation was initiated successfully
   * @throws IllegalStateException if a {@link BluetoothGattCallback} has not been set by {@link
   *     ShadowBluetoothGatt#setGattCallback}
   */
  public boolean readIncomingCharacteristic(BluetoothGattCharacteristic characteristic) {
    if (this.getGattCallback() == null) {
      throw new IllegalStateException(NULL_CALLBACK_MSG);
    }
    if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) == 0
        || characteristic.getService() == null) {
      return false;
    }

    this.readBytes = characteristic.getValue();
    this.bluetoothGattCallback.onCharacteristicRead(
        this.realBluetoothGatt, characteristic, BluetoothGatt.GATT_SUCCESS);
    return true;
  }

  public void addDiscoverableService(BluetoothGattService service) {
    this.discoverableServices.add(service);
  }

  public void removeDiscoverableService(BluetoothGattService service) {
    this.discoverableServices.remove(service);
  }

  public BluetoothGattCallback getGattCallback() {
    return this.bluetoothGattCallback;
  }

  public void setGattCallback(BluetoothGattCallback bluetoothGattCallback) {
    this.bluetoothGattCallback = bluetoothGattCallback;
  }

  public boolean isConnected() {
    return this.isConnected;
  }

  public boolean isClosed() {
    return this.isClosed;
  }

  public int getConnectionPriority() {
    return this.connectionPriority;
  }

  public byte[] getLatestWrittenBytes() {
    return this.writtenBytes;
  }

  public byte[] getLatestReadBytes() {
    return this.readBytes;
  }

  public BluetoothConnectionManager getBluetoothConnectionManager() {
    return BluetoothConnectionManager.getInstance();
  }

  /**
   * Simulate a successful Gatt Client Conection with {@link BluetoothConnectionManager}. Performs a
   * {@link BluetoothGattCallback#onConnectionStateChange} if available.
   *
   * @param remoteAddress address of Gatt client
   */
  public void notifyConnection(String remoteAddress) {
    BluetoothConnectionManager.getInstance().registerGattClientConnection(remoteAddress);
    this.isConnected = true;
    if (this.isCallbackAppropriate()) {
      this.getGattCallback()
          .onConnectionStateChange(
              this.realBluetoothGatt, BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
    }
  }

  /**
   * Simulate a successful Gatt Client Disconnection with {@link BluetoothConnectionManager}.
   * Performs a {@link BluetoothGattCallback#onConnectionStateChange} if available.
   *
   * @param remoteAddress address of Gatt client
   */
  public void notifyDisconnection(String remoteAddress) {
    BluetoothConnectionManager.getInstance().unregisterGattClientConnection(remoteAddress);
    if (this.isCallbackAppropriate()) {
      this.getGattCallback()
          .onConnectionStateChange(
              this.realBluetoothGatt,
              BluetoothGatt.GATT_SUCCESS,
              BluetoothProfile.STATE_DISCONNECTED);
    }
    this.isConnected = false;
  }

  private boolean isCallbackAppropriate() {
    return this.getGattCallback() != null && this.isConnected;
  }

  @ForType(BluetoothGatt.class)
  private interface BluetoothGattReflector {

    @Direct
    void disconnect();

    @Direct
    void close();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy