blob: c0fecb31d41ea202b1968e24c59ebc17e976ddb3 [file] [log] [blame]
/*
* Copyright (C) 2018 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.car.trust.client;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.os.Handler;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* A simple client that supports the scanning and connecting to available BLE devices. Should be
* used along with {@link SimpleBleServer}.
*/
public class SimpleBleClient {
public interface ClientCallback {
/**
* Called when a device that has a matching service UUID is found.
**/
void onDeviceConnected(BluetoothDevice device);
void onDeviceDisconnected();
void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic);
/**
* Called for each {@link BluetoothGattService} that is discovered on the
* {@link BluetoothDevice} after a matching scan result and connection.
*
* @param service {@link BluetoothGattService} that has been discovered.
*/
void onServiceDiscovered(BluetoothGattService service);
}
/**
* Wrapper class to allow queuing of BLE actions. The BLE stack allows only one action to be
* executed at a time.
*/
public static class BleAction {
public static final int ACTION_WRITE = 0;
public static final int ACTION_READ = 1;
private int mAction;
private BluetoothGattCharacteristic mCharacteristic;
public BleAction(BluetoothGattCharacteristic characteristic, int action) {
mAction = action;
mCharacteristic = characteristic;
}
public int getAction() {
return mAction;
}
public BluetoothGattCharacteristic getCharacteristic() {
return mCharacteristic;
}
}
private static final long SCAN_TIME_MS = 10000;
private final Queue<BleAction> mBleActionQueue = new ConcurrentLinkedQueue<BleAction>();
private final List<ClientCallback> mCallbacks = new ArrayList<>();
private final Context mContext;
private final BluetoothLeScanner mScanner;
private BluetoothGatt mBtGatt;
private ParcelUuid mServiceUuid;
public SimpleBleClient(@NonNull Context context) {
mContext = context;
BluetoothManager btManager = (BluetoothManager) mContext.getSystemService(
Context.BLUETOOTH_SERVICE);
mScanner = btManager.getAdapter().getBluetoothLeScanner();
}
/**
* Start scanning for a BLE devices with the specified service uuid.
*
* @param parcelUuid {@link ParcelUuid} used to identify the device that should be used for
* this client. This uuid should be the same as the one that is set in the
* {@link android.bluetooth.le.AdvertiseData.Builder} by the advertising
* device.
*/
public void start(ParcelUuid parcelUuid) {
mServiceUuid = parcelUuid;
// We only want to scan for devices that have the correct uuid set in its advertise data.
List<ScanFilter> filters = new ArrayList<ScanFilter>();
ScanFilter.Builder serviceFilter = new ScanFilter.Builder();
serviceFilter.setServiceUuid(mServiceUuid);
filters.add(serviceFilter.build());
ScanSettings.Builder settings = new ScanSettings.Builder();
settings.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
Log.d(Utils.LOG_TAG, "Start scanning for uuid: " + mServiceUuid.getUuid());
mScanner.startScan(filters, settings.build(), mScanCallback);
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
mScanner.stopScan(mScanCallback);
Log.d(Utils.LOG_TAG, "Stopping Scanner");
}
}, SCAN_TIME_MS);
}
private boolean hasServiceUuid(ScanResult result) {
if (result.getScanRecord() == null
|| result.getScanRecord().getServiceUuids() == null
|| result.getScanRecord().getServiceUuids().size() == 0) {
return false;
}
return true;
}
/**
* Writes to a {@link BluetoothGattCharacteristic} if possible, or queues the action until
* other actions are complete.
*
* @param characteristic {@link BluetoothGattCharacteristic} to be written
*/
public void writeCharacteristic(BluetoothGattCharacteristic characteristic) {
processAction(new BleAction(characteristic, BleAction.ACTION_WRITE));
}
/**
* Reads a {@link BluetoothGattCharacteristic} if possible, or queues the read action until
* other actions are complete.
*
* @param characteristic {@link BluetoothGattCharacteristic} to be read.
*/
public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
processAction(new BleAction(characteristic, BleAction.ACTION_READ));
}
/**
* Enable or disable notification for specified {@link BluetoothGattCharacteristic}.
*
* @param characteristic The {@link BluetoothGattCharacteristic} for which to enable
* notifications.
* @param enabled True if notifications should be enabled, false otherwise.
*/
public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
boolean enabled) {
mBtGatt.setCharacteristicNotification(characteristic, enabled);
}
/**
* Add a {@link ClientCallback} to listen for updates from BLE components
*/
public void addCallback(ClientCallback callback) {
mCallbacks.add(callback);
}
public void removeCallback(ClientCallback callback) {
mCallbacks.remove(callback);
}
private void processAction(BleAction action) {
// Only execute actions if the queue is empty.
if (mBleActionQueue.size() > 0) {
mBleActionQueue.add(action);
return;
}
mBleActionQueue.add(action);
executeAction(mBleActionQueue.peek());
}
private void processNextAction() {
mBleActionQueue.poll();
executeAction(mBleActionQueue.peek());
}
private void executeAction(BleAction action) {
if (action == null) {
return;
}
Log.d(Utils.LOG_TAG, "Executing BLE Action type: " + action.getAction());
int actionType = action.getAction();
switch (actionType) {
case BleAction.ACTION_WRITE:
mBtGatt.writeCharacteristic(action.getCharacteristic());
break;
case BleAction.ACTION_READ:
mBtGatt.readCharacteristic(action.getCharacteristic());
break;
default:
}
}
private String getStatus(int status) {
switch (status) {
case BluetoothGatt.GATT_FAILURE:
return "Failure";
case BluetoothGatt.GATT_SUCCESS:
return "GATT_SUCCESS";
case BluetoothGatt.GATT_READ_NOT_PERMITTED:
return "GATT_READ_NOT_PERMITTED";
case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
return "GATT_WRITE_NOT_PERMITTED";
case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
return "GATT_INSUFFICIENT_AUTHENTICATION";
case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED:
return "GATT_REQUEST_NOT_SUPPORTED";
case BluetoothGatt.GATT_INVALID_OFFSET:
return "GATT_INVALID_OFFSET";
case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH:
return "GATT_INVALID_ATTRIBUTE_LENGTH";
case BluetoothGatt.GATT_CONNECTION_CONGESTED:
return "GATT_CONNECTION_CONGESTED";
default:
return "unknown";
}
}
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();
Log.d(Utils.LOG_TAG, "Scan result found: " + result.getScanRecord().getServiceUuids());
if (!hasServiceUuid(result)) {
return;
}
for (ParcelUuid uuid : result.getScanRecord().getServiceUuids()) {
Log.d(Utils.LOG_TAG, "Scan result UUID: " + uuid);
if (uuid.equals(mServiceUuid)) {
// This client only supports connecting to one service.
// Once we find one, stop scanning and open a GATT connection to the device.
mScanner.stopScan(mScanCallback);
mBtGatt = device.connectGatt(mContext, false /* autoConnect */, mGattCallback);
return;
}
}
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
for (ScanResult r : results) {
Log.d(Utils.LOG_TAG, "Batch scanResult: " + r.getDevice().getName()
+ " " + r.getDevice().getAddress());
}
}
@Override
public void onScanFailed(int errorCode) {
Log.e(Utils.LOG_TAG, "Scan failed: " + errorCode);
}
};
private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
String state = "";
if (newState == BluetoothProfile.STATE_CONNECTED) {
state = "Connected";
mBtGatt.discoverServices();
for (ClientCallback callback : mCallbacks) {
callback.onDeviceConnected(gatt.getDevice());
}
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
state = "Disconnected";
for (ClientCallback callback : mCallbacks) {
callback.onDeviceDisconnected();
}
}
Log.d(Utils.LOG_TAG, "Gatt connection status: " + getStatus(status)
+ " newState: " + state);
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
Log.d(Utils.LOG_TAG, "onServicesDiscovered: " + status);
List<BluetoothGattService> services = gatt.getServices();
if (services == null || services.size() <= 0) {
return;
}
// Notify clients of newly discovered services.
for (BluetoothGattService service : mBtGatt.getServices()) {
Log.d(Utils.LOG_TAG, "Found service: " + service.getUuid() + " notifying clients");
for (ClientCallback callback : mCallbacks) {
callback.onServiceDiscovered(service);
}
}
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
Log.d(Utils.LOG_TAG, "onCharacteristicWrite: " + status);
processNextAction();
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
Log.d(Utils.LOG_TAG, "onCharacteristicRead:" + new String(characteristic.getValue()));
processNextAction();
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
for (ClientCallback callback : mCallbacks) {
callback.onCharacteristicChanged(gatt, characteristic);
}
processNextAction();
}
};
}