blob: 0099475b52f411d01beb5e3c3110b111628b9d81 [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.android.impl.google.discovery.plugins.ble;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import android.util.Log;
import com.google.common.collect.Queues;
import java.util.ArrayDeque;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* A reader that reads Vanadium Gatt services from remote Gatt servers.
*/
class GattReader extends BluetoothGattCallback {
private static final String TAG = Driver.TAG;
// A handler that will get called when a GATT service is read.
interface Handler {
void onGattRead(BluetoothDevice device, BluetoothGattService service);
void onGattReadFailed(BluetoothDevice device);
}
// TODO(jhahn): What's the maximum MTU size in Android?
// Android seems to support up to 517 bytes. But there is no documentation on it.
private static final int MTU = 512;
// We serialize all Gatt requests. We cancel the request if it takes too
// long or hangs in order to prevent it from blocking other tasks.
//
// TODO(jhahn): Revisit the timeout.
private static final long GATT_TIMEOUT_MS = 10000; // 10 seconds.
private final Context mContext;
private final ScheduledThreadPoolExecutor mExecutor;
private final Set<UUID> mScanUuids;
private final Handler mHandler;
private final ArrayDeque<BluetoothDevice> mPendingReads;
private final Runnable mCancelTask;
private BluetoothDevice mCurrentDevice;
private BluetoothGatt mCurrentGatt;
private ScheduledFuture mCurrentGattTimeout;
private BluetoothGattService mCurrentService;
private Iterator<BluetoothGattService> mCurrentServiceIterator;
private Iterator<BluetoothGattCharacteristic> mCurrentCharacteristicIterator;
GattReader(Context context, Set<UUID> scanUuids, Handler handler) {
mContext = context;
mExecutor = new ScheduledThreadPoolExecutor(1);
mScanUuids = scanUuids;
mHandler = handler;
mPendingReads = Queues.newArrayDeque();
mCancelTask =
new Runnable() {
@Override
public void run() {
Log.e(TAG, "gatt operation timed out: " + mCurrentDevice);
cancelAndMaybeReadNextDevice();
}
};
}
/**
* Reads a specified service from a remote device as well as their characteristics.
* <p/>
* This is an asynchronous operation. Once service read is completed, the onGattRead() or
* onServiceReadFailed() callback is triggered.
*/
synchronized void readDevice(BluetoothDevice device) {
mPendingReads.add(device);
if (mCurrentDevice == null) {
maybeReadNextDevice();
}
}
/**
* Closes the Gatt reader cancelling the current read and deleting all pending requests.
*/
synchronized void close() {
if (mCurrentGatt != null) {
mCurrentGatt.close();
}
mExecutor.shutdown();
mPendingReads.clear();
}
private synchronized void maybeReadNextDevice() {
mCurrentGatt = null;
mCurrentGattTimeout = null;
mCurrentService = null;
mCurrentServiceIterator = null;
mCurrentCharacteristicIterator = null;
mCurrentDevice = mPendingReads.poll();
if (mCurrentDevice == null) {
return;
}
mCurrentGatt = mCurrentDevice.connectGatt(mContext, false, this);
mCurrentGattTimeout =
mExecutor.schedule(mCancelTask, GATT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
private synchronized void finishAndMaybeReadNextDevice() {
mCurrentGattTimeout.cancel(false);
mCurrentGatt.disconnect();
maybeReadNextDevice();
}
private synchronized void cancelAndMaybeReadNextDevice() {
mCurrentGattTimeout.cancel(false);
mCurrentGatt.close();
final BluetoothDevice device = mCurrentDevice;
mExecutor.submit(
new Runnable() {
@Override
public void run() {
mHandler.onGattReadFailed(device);
}
});
maybeReadNextDevice();
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState != BluetoothGatt.STATE_CONNECTED) {
// Connection is disconnected. Release it.
gatt.close();
return;
}
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "connectGatt failed: " + mCurrentDevice + " , status: " + status);
cancelAndMaybeReadNextDevice();
return;
}
// MTU exchange is not allowed on a BR/EDR physical link.
// (Bluetooth Core Specification Volume 3, Part G, 4.3.1)
//
// There is no way to get the actual link type. So we use the device type for it.
// It is not clear whether DEVICE_TYPE_DUAL is on a BR/EDR physical link, but
// it is safe to not exchange MTU for that type too.
int deviceType = mCurrentDevice.getType();
if (deviceType != BluetoothDevice.DEVICE_TYPE_CLASSIC
&& deviceType != BluetoothDevice.DEVICE_TYPE_DUAL) {
if (!gatt.requestMtu(MTU)) {
Log.e(TAG, "requestMtu failed: " + mCurrentDevice);
cancelAndMaybeReadNextDevice();
}
} else {
if (!gatt.discoverServices()) {
Log.e(TAG, "discoverServices failed: " + mCurrentDevice);
cancelAndMaybeReadNextDevice();
}
}
}
@Override
public synchronized void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.w(TAG, "requestMtu failed: " + mCurrentDevice + ", status: " + status);
cancelAndMaybeReadNextDevice();
return;
}
if (!gatt.discoverServices()) {
Log.e(TAG, "discoverServices failed: " + mCurrentDevice);
cancelAndMaybeReadNextDevice();
}
}
@Override
public synchronized void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "discoverServices failed: " + mCurrentDevice + ", status: " + status);
cancelAndMaybeReadNextDevice();
return;
}
mCurrentServiceIterator = gatt.getServices().iterator();
maybeReadNextService();
}
private void maybeReadNextService() {
while (mCurrentServiceIterator.hasNext()) {
mCurrentService = mCurrentServiceIterator.next();
if (mScanUuids.isEmpty() || mScanUuids.contains(mCurrentService.getUuid())) {
// Reset the timer.
if (!mCurrentGattTimeout.cancel(false)) {
// Already cancelled.
return;
}
mCurrentGattTimeout =
mExecutor.schedule(mCancelTask, GATT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
mCurrentCharacteristicIterator = mCurrentService.getCharacteristics().iterator();
maybeReadNextCharacteristic();
return;
}
}
// All services have been read. Finish the current device read.
finishAndMaybeReadNextDevice();
}
@Override
public synchronized void onCharacteristicRead(
BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "readCharacteristic failed: " + mCurrentDevice + ", status: " + status);
cancelAndMaybeReadNextDevice();
return;
}
maybeReadNextCharacteristic();
}
private void maybeReadNextCharacteristic() {
if (!mCurrentCharacteristicIterator.hasNext()) {
// All characteristics have been read. Finish the current service read.
final BluetoothDevice device = mCurrentDevice;
final BluetoothGattService service = mCurrentService;
mExecutor.submit(
new Runnable() {
@Override
public void run() {
mHandler.onGattRead(device, service);
}
});
maybeReadNextService();
return;
}
BluetoothGattCharacteristic characteristic = mCurrentCharacteristicIterator.next();
if (!mCurrentGatt.readCharacteristic(characteristic)) {
Log.e(TAG, "readCharacteristic failed: " + mCurrentDevice);
cancelAndMaybeReadNextDevice();
}
}
}