blob: ebc87a09159a8ed14f90cb1bb0d65c5234f9e5f1 [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.os.Build;
import android.util.Log;
import com.google.common.collect.Queues;
import java.lang.reflect.Method;
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 UUID mScanBaseUuid, mScanMaskUuid;
private final Handler mHandler;
private final ArrayDeque<BluetoothDevice> mPendingReads;
private BluetoothDevice mCurrentDevice;
private BluetoothGatt mCurrentGatt;
private ScheduledFuture mCurrentGattConnectionTimeout;
private BluetoothGattService mCurrentService;
private Iterator<BluetoothGattService> mCurrentServiceIterator;
private Iterator<BluetoothGattCharacteristic> mCurrentCharacteristicIterator;
/**
* Creates a new Gatt reader.
* <p/>
*
* An empty uuids means all Vanadium services and baseUuid and maskUuid will be used to
* filter Vanadium services.
*/
GattReader(Context context, Set<UUID> uuids, UUID baseUuid, UUID maskUuid, Handler handler) {
mContext = context;
mExecutor = new ScheduledThreadPoolExecutor(1);
mExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
mScanUuids = uuids;
mScanBaseUuid = baseUuid;
mScanMaskUuid = maskUuid;
mHandler = handler;
mPendingReads = Queues.newArrayDeque();
}
/**
* 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(boolean graceful) {
mPendingReads.clear();
if (graceful) {
// Wait until the current read finishes to avoid messing up the Bluetooth stack.
while (mCurrentDevice != null) {
try {
wait();
} catch (InterruptedException e) {
break;
}
}
}
mExecutor.shutdown();
if (mCurrentGatt != null) {
mCurrentGatt.close();
}
}
private synchronized void maybeReadNextDevice() {
mCurrentGatt = null;
mCurrentGattConnectionTimeout = null;
mCurrentService = null;
mCurrentServiceIterator = null;
mCurrentCharacteristicIterator = null;
mCurrentDevice = mPendingReads.poll();
if (mCurrentDevice == null) {
notifyAll();
return;
}
mCurrentGatt = mCurrentDevice.connectGatt(mContext, false, this);
mCurrentGattConnectionTimeout =
mExecutor.schedule(
new Runnable() {
@Override
public void run() {
Log.e(TAG, "gatt connection timed out: " + mCurrentDevice);
cancelAndMaybeReadNextDevice();
}
},
GATT_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
}
private synchronized void finishAndMaybeReadNextDevice() {
mCurrentGattConnectionTimeout.cancel(false);
mCurrentGatt.close();
maybeReadNextDevice();
}
private synchronized void cancelAndMaybeReadNextDevice() {
mCurrentGattConnectionTimeout.cancel(false);
// Try to refresh the failed Gatt.
try {
Method method = mCurrentGatt.getClass().getMethod("refresh", new Class[0]);
if (method != null) {
method.invoke(mCurrentGatt, new Object[0]);
}
} catch (Exception e) {
Log.e(TAG, "An exception occured while refreshing device");
}
mCurrentGatt.close();
final BluetoothDevice device = mCurrentDevice;
mExecutor.submit(
new Runnable() {
@Override
public void run() {
mHandler.onGattReadFailed(device);
}
});
maybeReadNextDevice();
}
@Override
public synchronized void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (gatt != mCurrentGatt) {
// This must be for an old Gatt connection which has been already cancelled. Ignore it.
gatt.close();
return;
}
if (status != BluetoothGatt.GATT_SUCCESS || newState != BluetoothGatt.STATE_CONNECTED) {
Log.e(TAG, "connection failed: " + mCurrentDevice + " , status: " + status);
cancelAndMaybeReadNextDevice();
return;
}
// Reset the connection timer.
if (!mCurrentGattConnectionTimeout.cancel(false)) {
// Already cancelled.
return;
}
// TODO(jhahn): Do we really need this?
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
}
// 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 (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP
&& 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 void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.w(TAG, "onMtuChanged failed: " + mCurrentDevice + ", status: " + status);
cancelAndMaybeReadNextDevice();
return;
}
if (!gatt.discoverServices()) {
Log.e(TAG, "discoverServices failed: " + mCurrentDevice);
cancelAndMaybeReadNextDevice();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "onServicesDiscovered failed: " + mCurrentDevice + ", status: " + status);
cancelAndMaybeReadNextDevice();
return;
}
mCurrentServiceIterator = gatt.getServices().iterator();
maybeReadNextService();
}
private boolean isTargetService(UUID uuid) {
if (mScanUuids.contains(uuid)) {
return true;
}
return mScanUuids.isEmpty()
&& (uuid.getMostSignificantBits() & mScanMaskUuid.getMostSignificantBits())
== mScanBaseUuid.getMostSignificantBits()
&& (uuid.getLeastSignificantBits() & mScanMaskUuid.getLeastSignificantBits())
== mScanBaseUuid.getLeastSignificantBits();
}
private void maybeReadNextService() {
while (mCurrentServiceIterator.hasNext()) {
mCurrentService = mCurrentServiceIterator.next();
if (!isTargetService(mCurrentService.getUuid())) {
continue;
}
mCurrentCharacteristicIterator = mCurrentService.getCharacteristics().iterator();
maybeReadNextCharacteristic();
return;
}
// All services have been read. Finish the current device read.
finishAndMaybeReadNextDevice();
}
@Override
public void onCharacteristicRead(
BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "onCharacteristicRead 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();
}
}
}