blob: 18a9f5f3bbbef35ee98e34e9ab50f2693c4ed9ea [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.nearby.common.bluetooth.gatt;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import com.android.server.nearby.common.bluetooth.BluetoothException;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothDevice;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattCallback;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothGattWrapper;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.BluetoothLeScanner;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanCallback;
import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.le.ScanResult;
import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException;
import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.Operation;
import com.google.common.base.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* Wrapper of {@link BluetoothGattWrapper} that provides blocking methods, errors and timeout
* handling.
*/
@SuppressWarnings("Guava") // java.util.Optional is not available until API 24
public class BluetoothGattHelper {
private static final String TAG = BluetoothGattHelper.class.getSimpleName();
@VisibleForTesting
static final long LOW_LATENCY_SCAN_MILLIS = TimeUnit.SECONDS.toMillis(5);
private static final long POLL_INTERVAL_MILLIS = 5L /* milliseconds */;
/**
* BT operation types that can be in flight.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(
value = {
OperationType.SCAN,
OperationType.CONNECT,
OperationType.DISCOVER_SERVICES,
OperationType.DISCOVER_SERVICES_INTERNAL,
OperationType.NOTIFICATION_CHANGE,
OperationType.READ_CHARACTERISTIC,
OperationType.WRITE_CHARACTERISTIC,
OperationType.READ_DESCRIPTOR,
OperationType.WRITE_DESCRIPTOR,
OperationType.READ_RSSI,
OperationType.WRITE_RELIABLE,
OperationType.CHANGE_MTU,
OperationType.DISCONNECT,
})
public @interface OperationType {
int SCAN = 0;
int CONNECT = 1;
int DISCOVER_SERVICES = 2;
int DISCOVER_SERVICES_INTERNAL = 3;
int NOTIFICATION_CHANGE = 4;
int READ_CHARACTERISTIC = 5;
int WRITE_CHARACTERISTIC = 6;
int READ_DESCRIPTOR = 7;
int WRITE_DESCRIPTOR = 8;
int READ_RSSI = 9;
int WRITE_RELIABLE = 10;
int CHANGE_MTU = 11;
int DISCONNECT = 12;
}
@VisibleForTesting
final ScanCallback mScanCallback = new InternalScanCallback();
@VisibleForTesting
final BluetoothGattCallback mBluetoothGattCallback =
new InternalBluetoothGattCallback();
@VisibleForTesting
final ConcurrentMap<BluetoothGattWrapper, BluetoothGattConnection> mConnections =
new ConcurrentHashMap<>();
private final Context mApplicationContext;
private final BluetoothAdapter mBluetoothAdapter;
private final BluetoothOperationExecutor mBluetoothOperationExecutor;
@VisibleForTesting
BluetoothGattHelper(
Context applicationContext,
BluetoothAdapter bluetoothAdapter,
BluetoothOperationExecutor bluetoothOperationExecutor) {
mApplicationContext = applicationContext;
mBluetoothAdapter = bluetoothAdapter;
mBluetoothOperationExecutor = bluetoothOperationExecutor;
}
public BluetoothGattHelper(Context applicationContext, BluetoothAdapter bluetoothAdapter) {
this(
Preconditions.checkNotNull(applicationContext),
Preconditions.checkNotNull(bluetoothAdapter),
new BluetoothOperationExecutor(5));
}
/**
* Auto-connects a serice Uuid.
*/
public BluetoothGattConnection autoConnect(final UUID serviceUuid) throws BluetoothException {
Log.d(TAG, String.format("Starting autoconnection to a device advertising service %s.",
serviceUuid));
BluetoothDevice device = null;
int retries = 3;
final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
if (scanner == null) {
throw new BluetoothException("Bluetooth is disabled or LE is not supported.");
}
final ScanFilter serviceFilter = new ScanFilter.Builder()
.setServiceUuid(new ParcelUuid(serviceUuid))
.build();
ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder()
.setReportDelay(0);
final ScanSettings scanSettingsLowLatency = scanSettingsBuilder
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
final ScanSettings scanSettingsLowPower = scanSettingsBuilder
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.build();
while (true) {
long startTimeMillis = System.currentTimeMillis();
try {
Log.d(TAG, "Starting low latency scanning.");
device =
mBluetoothOperationExecutor.executeNonnull(
new Operation<BluetoothDevice>(OperationType.SCAN) {
@Override
public void run() throws BluetoothException {
scanner.startScan(Arrays.asList(serviceFilter),
scanSettingsLowLatency, mScanCallback);
}
}, LOW_LATENCY_SCAN_MILLIS);
} catch (BluetoothOperationTimeoutException e) {
Log.d(TAG, String.format(
"Cannot find a nearby device in low latency scanning after %s ms.",
LOW_LATENCY_SCAN_MILLIS));
} finally {
scanner.stopScan(mScanCallback);
}
if (device == null) {
Log.d(TAG, "Starting low power scanning.");
try {
device = mBluetoothOperationExecutor.executeNonnull(
new Operation<BluetoothDevice>(OperationType.SCAN) {
@Override
public void run() throws BluetoothException {
scanner.startScan(Arrays.asList(serviceFilter),
scanSettingsLowPower, mScanCallback);
}
});
} finally {
scanner.stopScan(mScanCallback);
}
}
Log.d(TAG, String.format("Scanning done in %d ms. Found device %s.",
System.currentTimeMillis() - startTimeMillis, device));
try {
return connect(device);
} catch (BluetoothException e) {
retries--;
if (retries == 0) {
throw e;
} else {
Log.d(TAG, String.format(
"Connection failed: %s. Retrying %d more times.", e, retries));
}
}
}
}
/**
* Connects to a device using default connection options.
*/
public BluetoothGattConnection connect(BluetoothDevice bluetoothDevice)
throws BluetoothException {
return connect(bluetoothDevice, ConnectionOptions.builder().build());
}
/**
* Connects to a device using specifies connection options.
*/
public BluetoothGattConnection connect(
BluetoothDevice bluetoothDevice, ConnectionOptions options) throws BluetoothException {
Log.d(TAG, String.format("Connecting to device %s.", bluetoothDevice));
long startTimeMillis = System.currentTimeMillis();
Operation<BluetoothGattConnection> connectOperation =
new Operation<BluetoothGattConnection>(OperationType.CONNECT, bluetoothDevice) {
private final Object mLock = new Object();
@GuardedBy("mLock")
private boolean mIsCanceled = false;
@GuardedBy("mLock")
@Nullable(/* null before operation is executed */)
private BluetoothGattWrapper mBluetoothGatt;
@Override
public void run() throws BluetoothException {
synchronized (mLock) {
if (mIsCanceled) {
return;
}
BluetoothGattWrapper bluetoothGattWrapper;
Log.d(TAG, "Use LE transport");
bluetoothGattWrapper =
bluetoothDevice.connectGatt(
mApplicationContext,
options.autoConnect(),
mBluetoothGattCallback,
android.bluetooth.BluetoothDevice.TRANSPORT_LE);
if (bluetoothGattWrapper == null) {
throw new BluetoothException("connectGatt() returned null.");
}
try {
// Set connection priority without waiting for connection callback.
// Per code, btif_gatt_client.c, when priority is set before
// connection, this sets preferred connection parameters that will
// be used during the connection establishment.
Optional<Integer> connectionPriorityOption =
options.connectionPriority();
if (connectionPriorityOption.isPresent()) {
// requestConnectionPriority can only be called when
// BluetoothGatt is connected to the system BluetoothGatt
// service (see android/bluetooth/BluetoothGatt.java code).
// However, there is no callback to the app to inform when this
// is done. requestConnectionPriority will returns false with no
// side-effect before the service is connected, so we just poll
// here until true is returned.
int connectionPriority = connectionPriorityOption.get();
long startTimeMillis = System.currentTimeMillis();
while (!bluetoothGattWrapper.requestConnectionPriority(
connectionPriority)) {
if (System.currentTimeMillis() - startTimeMillis
> options.connectionTimeoutMillis()) {
throw new BluetoothException(
String.format(
Locale.US,
"Failed to set connectionPriority "
+ "after %dms.",
options.connectionTimeoutMillis()));
}
try {
Thread.sleep(POLL_INTERVAL_MILLIS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BluetoothException(
"connect() operation interrupted.");
}
}
}
} catch (Exception e) {
// Make sure to clean connection.
bluetoothGattWrapper.disconnect();
bluetoothGattWrapper.close();
throw e;
}
BluetoothGattConnection connection = new BluetoothGattConnection(
bluetoothGattWrapper, mBluetoothOperationExecutor, options);
mConnections.put(bluetoothGattWrapper, connection);
mBluetoothGatt = bluetoothGattWrapper;
}
}
@Override
public void cancel() {
// Clean connection if connection times out.
synchronized (mLock) {
if (mIsCanceled) {
return;
}
mIsCanceled = true;
BluetoothGattWrapper bluetoothGattWrapper = mBluetoothGatt;
if (bluetoothGattWrapper == null) {
return;
}
mConnections.remove(bluetoothGattWrapper);
bluetoothGattWrapper.disconnect();
bluetoothGattWrapper.close();
}
}
};
BluetoothGattConnection result;
if (options.autoConnect()) {
result = mBluetoothOperationExecutor.executeNonnull(connectOperation);
} else {
result =
mBluetoothOperationExecutor.executeNonnull(
connectOperation, options.connectionTimeoutMillis());
}
Log.d(TAG, String.format("Connection success in %d ms.",
System.currentTimeMillis() - startTimeMillis));
return result;
}
private BluetoothGattConnection getConnectionByGatt(BluetoothGattWrapper gatt)
throws BluetoothException {
BluetoothGattConnection connection = mConnections.get(gatt);
if (connection == null) {
throw new BluetoothException("Receive callback on unexpected device: " + gatt);
}
return connection;
}
private class InternalBluetoothGattCallback extends BluetoothGattCallback {
@Override
public void onConnectionStateChange(BluetoothGattWrapper gatt, int status, int newState) {
BluetoothGattConnection connection;
BluetoothDevice device = gatt.getDevice();
switch (newState) {
case BluetoothGatt.STATE_CONNECTED: {
connection = mConnections.get(gatt);
if (connection == null) {
Log.w(TAG, String.format(
"Received unexpected successful connection for dev %s! Ignoring.",
device));
break;
}
Operation<BluetoothGattConnection> operation =
new Operation<>(OperationType.CONNECT, device);
if (status != BluetoothGatt.GATT_SUCCESS) {
mConnections.remove(gatt);
gatt.disconnect();
gatt.close();
mBluetoothOperationExecutor.notifyCompletion(operation, status, null);
break;
}
// Process connection options
ConnectionOptions options = connection.getConnectionOptions();
Optional<Integer> mtuOption = options.mtu();
if (mtuOption.isPresent()) {
// Requesting MTU and waiting for MTU callback.
boolean success = gatt.requestMtu(mtuOption.get());
if (!success) {
mBluetoothOperationExecutor.notifyFailure(operation,
new BluetoothException(String.format(Locale.US,
"Failed to request MTU of %d for dev %s: "
+ "returned false.",
mtuOption.get(), device)));
// Make sure to clean connection.
mConnections.remove(gatt);
gatt.disconnect();
gatt.close();
}
break;
}
// Connection successful
connection.onConnected();
mBluetoothOperationExecutor.notifyCompletion(operation, status, connection);
break;
}
case BluetoothGatt.STATE_DISCONNECTED: {
connection = mConnections.remove(gatt);
if (connection == null) {
Log.w(TAG, String.format("Received unexpected disconnection"
+ " for device %s! Ignoring.", device));
break;
}
if (!connection.isConnected()) {
// This is a failed connection attempt
if (status == BluetoothGatt.GATT_SUCCESS) {
// This is weird... considering this as a failure
Log.w(TAG, String.format(
"Received a success for a failed connection "
+ "attempt for device %s! Ignoring.", device));
status = BluetoothGatt.GATT_FAILURE;
}
mBluetoothOperationExecutor
.notifyCompletion(new Operation<BluetoothGattConnection>(
OperationType.CONNECT, device), status, null);
// Clean Gatt object in every case.
gatt.disconnect();
gatt.close();
break;
}
connection.onClosed();
mBluetoothOperationExecutor.notifyCompletion(
new Operation<>(OperationType.DISCONNECT, device), status);
break;
}
default:
Log.e(TAG, "Unexpected connection state: " + newState);
}
}
@Override
public void onMtuChanged(BluetoothGattWrapper gatt, int mtu, int status) {
BluetoothGattConnection connection = mConnections.get(gatt);
BluetoothDevice device = gatt.getDevice();
if (connection == null) {
Log.w(TAG, String.format(
"Received unexpected MTU change for device %s! Ignoring.", device));
return;
}
if (connection.isConnected()) {
// This is the callback for the deprecated BluetoothGattConnection.requestMtu.
mBluetoothOperationExecutor.notifyCompletion(
new Operation<>(OperationType.CHANGE_MTU, gatt), status, mtu);
} else {
// This is the callback when requesting MTU right after connecting.
connection.onConnected();
mBluetoothOperationExecutor.notifyCompletion(
new Operation<>(OperationType.CONNECT, device), status, connection);
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.w(TAG, String.format(
"%s responds MTU change failed, status %s.", device, status));
// Clean connection if it's failed.
mConnections.remove(gatt);
gatt.disconnect();
gatt.close();
return;
}
}
}
@Override
public void onServicesDiscovered(BluetoothGattWrapper gatt, int status) {
mBluetoothOperationExecutor.notifyCompletion(
new Operation<Void>(OperationType.DISCOVER_SERVICES_INTERNAL, gatt), status);
}
@Override
public void onCharacteristicRead(BluetoothGattWrapper gatt,
BluetoothGattCharacteristic characteristic, int status) {
mBluetoothOperationExecutor.notifyCompletion(
new Operation<byte[]>(OperationType.READ_CHARACTERISTIC, gatt, characteristic),
status, characteristic.getValue());
}
@Override
public void onCharacteristicWrite(BluetoothGattWrapper gatt,
BluetoothGattCharacteristic characteristic, int status) {
mBluetoothOperationExecutor.notifyCompletion(new Operation<Void>(
OperationType.WRITE_CHARACTERISTIC, gatt, characteristic), status);
}
@Override
public void onDescriptorRead(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor,
int status) {
mBluetoothOperationExecutor.notifyCompletion(
new Operation<byte[]>(OperationType.READ_DESCRIPTOR, gatt, descriptor), status,
descriptor.getValue());
}
@Override
public void onDescriptorWrite(BluetoothGattWrapper gatt, BluetoothGattDescriptor descriptor,
int status) {
Log.d(TAG, String.format("onDescriptorWrite %s, %s, %d",
gatt.getDevice(), descriptor.getUuid(), status));
mBluetoothOperationExecutor.notifyCompletion(
new Operation<Void>(OperationType.WRITE_DESCRIPTOR, gatt, descriptor), status);
}
@Override
public void onReadRemoteRssi(BluetoothGattWrapper gatt, int rssi, int status) {
mBluetoothOperationExecutor.notifyCompletion(
new Operation<Integer>(OperationType.READ_RSSI, gatt), status, rssi);
}
@Override
public void onReliableWriteCompleted(BluetoothGattWrapper gatt, int status) {
mBluetoothOperationExecutor.notifyCompletion(
new Operation<Void>(OperationType.WRITE_RELIABLE, gatt), status);
}
@Override
public void onCharacteristicChanged(BluetoothGattWrapper gatt,
BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
if (value == null) {
// Value is not supposed to be null, but just to be safe...
value = new byte[0];
}
Log.d(TAG, String.format("Characteristic %s changed, Gatt device: %s",
characteristic.getUuid(), gatt.getDevice()));
try {
getConnectionByGatt(gatt).onCharacteristicChanged(characteristic, value);
} catch (BluetoothException e) {
Log.e(TAG, "Error in onCharacteristicChanged", e);
}
}
}
private class InternalScanCallback extends ScanCallback {
@Override
public void onScanFailed(int errorCode) {
String errorMessage;
switch (errorCode) {
case ScanCallback.SCAN_FAILED_ALREADY_STARTED:
errorMessage = "SCAN_FAILED_ALREADY_STARTED";
break;
case ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
errorMessage = "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED";
break;
case ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED:
errorMessage = "SCAN_FAILED_FEATURE_UNSUPPORTED";
break;
case ScanCallback.SCAN_FAILED_INTERNAL_ERROR:
errorMessage = "SCAN_FAILED_INTERNAL_ERROR";
break;
default:
errorMessage = "Unknown error code - " + errorCode;
}
mBluetoothOperationExecutor.notifyFailure(
new Operation<BluetoothDevice>(OperationType.SCAN),
new BluetoothException("Scan failed: " + errorMessage));
}
@Override
public void onScanResult(int callbackType, ScanResult result) {
mBluetoothOperationExecutor.notifySuccess(
new Operation<BluetoothDevice>(OperationType.SCAN), result.getDevice());
}
}
/**
* Options for {@link #connect}.
*/
public static class ConnectionOptions {
private boolean mAutoConnect;
private long mConnectionTimeoutMillis;
private Optional<Integer> mConnectionPriority;
private Optional<Integer> mMtu;
private ConnectionOptions(boolean autoConnect, long connectionTimeoutMillis,
Optional<Integer> connectionPriority,
Optional<Integer> mtu) {
this.mAutoConnect = autoConnect;
this.mConnectionTimeoutMillis = connectionTimeoutMillis;
this.mConnectionPriority = connectionPriority;
this.mMtu = mtu;
}
boolean autoConnect() {
return mAutoConnect;
}
long connectionTimeoutMillis() {
return mConnectionTimeoutMillis;
}
Optional<Integer> connectionPriority() {
return mConnectionPriority;
}
Optional<Integer> mtu() {
return mMtu;
}
@Override
public String toString() {
return "ConnectionOptions{"
+ "autoConnect=" + mAutoConnect + ", "
+ "connectionTimeoutMillis=" + mConnectionTimeoutMillis + ", "
+ "connectionPriority=" + mConnectionPriority + ", "
+ "mtu=" + mMtu
+ "}";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof ConnectionOptions) {
ConnectionOptions that = (ConnectionOptions) o;
return this.mAutoConnect == that.autoConnect()
&& this.mConnectionTimeoutMillis == that.connectionTimeoutMillis()
&& this.mConnectionPriority.equals(that.connectionPriority())
&& this.mMtu.equals(that.mtu());
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(mAutoConnect, mConnectionTimeoutMillis, mConnectionPriority, mMtu);
}
/**
* Creates a builder of ConnectionOptions.
*/
public static Builder builder() {
return new ConnectionOptions.Builder()
.setAutoConnect(false)
.setConnectionTimeoutMillis(TimeUnit.SECONDS.toMillis(5));
}
/**
* Builder for {@link ConnectionOptions}.
*/
public static class Builder {
private boolean mAutoConnect;
private long mConnectionTimeoutMillis;
private Optional<Integer> mConnectionPriority = Optional.empty();
private Optional<Integer> mMtu = Optional.empty();
/**
* See {@link android.bluetooth.BluetoothDevice#connectGatt}.
*/
public Builder setAutoConnect(boolean autoConnect) {
this.mAutoConnect = autoConnect;
return this;
}
/**
* See {@link android.bluetooth.BluetoothGatt#requestConnectionPriority(int)}.
*/
public Builder setConnectionPriority(int connectionPriority) {
this.mConnectionPriority = Optional.of(connectionPriority);
return this;
}
/**
* See {@link android.bluetooth.BluetoothGatt#requestMtu(int)}.
*/
public Builder setMtu(int mtu) {
this.mMtu = Optional.of(mtu);
return this;
}
/**
* Sets the timeout for the GATT connection.
*/
public Builder setConnectionTimeoutMillis(long connectionTimeoutMillis) {
this.mConnectionTimeoutMillis = connectionTimeoutMillis;
return this;
}
/**
* Builds ConnectionOptions.
*/
public ConnectionOptions build() {
return new ConnectionOptions(mAutoConnect, mConnectionTimeoutMillis,
mConnectionPriority, mMtu);
}
}
}
}