Home | History | Annotate | Download | only in comms
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 package com.android.car.trust.comms;
     17 
     18 import android.bluetooth.BluetoothDevice;
     19 import android.bluetooth.BluetoothGatt;
     20 import android.bluetooth.BluetoothGattCallback;
     21 import android.bluetooth.BluetoothGattCharacteristic;
     22 import android.bluetooth.BluetoothGattService;
     23 import android.bluetooth.BluetoothManager;
     24 import android.bluetooth.BluetoothProfile;
     25 import android.bluetooth.le.BluetoothLeScanner;
     26 import android.bluetooth.le.ScanCallback;
     27 import android.bluetooth.le.ScanFilter;
     28 import android.bluetooth.le.ScanResult;
     29 import android.bluetooth.le.ScanSettings;
     30 import android.content.Context;
     31 import android.os.Handler;
     32 import android.os.ParcelUuid;
     33 import android.support.annotation.NonNull;
     34 import android.util.Log;
     35 
     36 import java.util.ArrayList;
     37 import java.util.List;
     38 import java.util.Queue;
     39 import java.util.concurrent.ConcurrentLinkedQueue;
     40 
     41 /**
     42  * A simple client that supports the scanning and connecting to available BLE devices. Should be
     43  * used along with {@link SimpleBleServer}.
     44  */
     45 public class SimpleBleClient {
     46     public interface ClientCallback {
     47         /**
     48          * Called when a device that has a matching service UUID is found.
     49          **/
     50         void onDeviceConnected(BluetoothDevice device);
     51 
     52         void onDeviceDisconnected();
     53 
     54         void onCharacteristicChanged(BluetoothGatt gatt,
     55                 BluetoothGattCharacteristic characteristic);
     56 
     57         /**
     58          * Called for each {@link BluetoothGattService} that is discovered on the
     59          * {@link BluetoothDevice} after a matching scan result and connection.
     60          *
     61          * @param service {@link BluetoothGattService} that has been discovered.
     62          */
     63         void onServiceDiscovered(BluetoothGattService service);
     64     }
     65 
     66     /**
     67      * Wrapper class to allow queuing of BLE actions. The BLE stack allows only one action to be
     68      * executed at a time.
     69      */
     70     public static class BleAction {
     71         public static final int ACTION_WRITE = 0;
     72         public static final int ACTION_READ = 1;
     73 
     74         private int mAction;
     75         private BluetoothGattCharacteristic mCharacteristic;
     76 
     77         public BleAction(BluetoothGattCharacteristic characteristic, int action) {
     78             mAction = action;
     79             mCharacteristic = characteristic;
     80         }
     81 
     82         public int getAction() {
     83             return mAction;
     84         }
     85 
     86         public BluetoothGattCharacteristic getCharacteristic() {
     87             return mCharacteristic;
     88         }
     89     }
     90 
     91     private static final String TAG = "SimpleBleClient";
     92     private static final long SCAN_TIME_MS = 10000;
     93 
     94     private Queue<BleAction> mBleActionQueue = new ConcurrentLinkedQueue<BleAction>();
     95 
     96     private BluetoothManager mBtManager;
     97     private BluetoothLeScanner mScanner;
     98 
     99     protected BluetoothGatt mBtGatt;
    100 
    101     private List<ClientCallback> mCallbacks;
    102     private ParcelUuid mServiceUuid;
    103     private Context mContext;
    104 
    105     public SimpleBleClient(@NonNull Context context) {
    106         mContext = context;
    107         mBtManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
    108         mScanner = mBtManager.getAdapter().getBluetoothLeScanner();
    109         mCallbacks = new ArrayList<>();
    110     }
    111 
    112     /**
    113      * Start scanning for a BLE devices with the specified service uuid.
    114      *
    115      * @param parcelUuid {@link ParcelUuid} used to identify the device that should be used for
    116      *                   this client. This uuid should be the same as the one that is set in the
    117      *                   {@link android.bluetooth.le.AdvertiseData.Builder} by the advertising
    118      *                   device.
    119      */
    120     public void start(ParcelUuid parcelUuid) {
    121         mServiceUuid = parcelUuid;
    122 
    123         // We only want to scan for devices that have the correct uuid set in its advertise data.
    124         List<ScanFilter> filters = new ArrayList<ScanFilter>();
    125         ScanFilter.Builder serviceFilter = new ScanFilter.Builder();
    126         serviceFilter.setServiceUuid(mServiceUuid);
    127         filters.add(serviceFilter.build());
    128 
    129         ScanSettings.Builder settings = new ScanSettings.Builder();
    130         settings.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
    131 
    132         if (Log.isLoggable(TAG, Log.DEBUG)) {
    133             Log.d(TAG, "Start scanning for uuid: " + mServiceUuid.getUuid());
    134         }
    135         mScanner.startScan(filters, settings.build(), mScanCallback);
    136 
    137         Handler handler = new Handler();
    138         handler.postDelayed(new Runnable() {
    139             @Override
    140             public void run() {
    141                 mScanner.stopScan(mScanCallback);
    142                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    143                     Log.d(TAG, "Stopping Scanner");
    144                 }
    145             }
    146         }, SCAN_TIME_MS);
    147     }
    148 
    149     private boolean hasServiceUuid(ScanResult result) {
    150         if (result.getScanRecord() == null
    151                 || result.getScanRecord().getServiceUuids() == null
    152                 || result.getScanRecord().getServiceUuids().size() == 0) {
    153             return false;
    154         }
    155         return true;
    156     }
    157 
    158     /**
    159      * Writes to a {@link BluetoothGattCharacteristic} if possible, or queues the action until
    160      * other actions are complete.
    161      *
    162      * @param characteristic {@link BluetoothGattCharacteristic} to be written
    163      */
    164     public void writeCharacteristic(BluetoothGattCharacteristic characteristic) {
    165         processAction(new BleAction(characteristic, BleAction.ACTION_WRITE));
    166     }
    167 
    168     /**
    169      * Reads a {@link BluetoothGattCharacteristic} if possible, or queues the read action until
    170      * other actions are complete.
    171      *
    172      * @param characteristic {@link BluetoothGattCharacteristic} to be read.
    173      */
    174     public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
    175         processAction(new BleAction(characteristic, BleAction.ACTION_READ));
    176     }
    177 
    178     /**
    179      * Enable or disable notification for specified {@link BluetoothGattCharacteristic}.
    180      *
    181      * @param characteristic The {@link BluetoothGattCharacteristic} for which to enable
    182      *                       notifications.
    183      * @param enabled        True if notifications should be enabled, false otherwise.
    184      */
    185     public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
    186             boolean enabled) {
    187         mBtGatt.setCharacteristicNotification(characteristic, enabled);
    188     }
    189 
    190     /**
    191      * Add a {@link ClientCallback} to listen for updates from BLE components
    192      */
    193     public void addCallback(ClientCallback callback) {
    194         mCallbacks.add(callback);
    195     }
    196 
    197     public void removeCallback(ClientCallback callback) {
    198         mCallbacks.remove(callback);
    199     }
    200 
    201     private void processAction(BleAction action) {
    202         // Only execute actions if the queue is empty.
    203         if (mBleActionQueue.size() > 0) {
    204             mBleActionQueue.add(action);
    205             return;
    206         }
    207 
    208         mBleActionQueue.add(action);
    209         executeAction(mBleActionQueue.peek());
    210     }
    211 
    212     private void processNextAction() {
    213         mBleActionQueue.poll();
    214         executeAction(mBleActionQueue.peek());
    215     }
    216 
    217     private void executeAction(BleAction action) {
    218         if (action == null) {
    219             return;
    220         }
    221 
    222         if (Log.isLoggable(TAG, Log.DEBUG)) {
    223             Log.d(TAG, "Executing BLE Action type: " + action.getAction());
    224         }
    225 
    226         int actionType = action.getAction();
    227         switch (actionType) {
    228             case BleAction.ACTION_WRITE:
    229                 mBtGatt.writeCharacteristic(action.getCharacteristic());
    230                 break;
    231             case BleAction.ACTION_READ:
    232                 mBtGatt.readCharacteristic(action.getCharacteristic());
    233                 break;
    234             default:
    235         }
    236     }
    237 
    238     private String getStatus(int status) {
    239         switch (status) {
    240             case BluetoothGatt.GATT_FAILURE:
    241                 return "Failure";
    242             case BluetoothGatt.GATT_SUCCESS:
    243                 return "GATT_SUCCESS";
    244             case BluetoothGatt.GATT_READ_NOT_PERMITTED:
    245                 return "GATT_READ_NOT_PERMITTED";
    246             case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
    247                 return "GATT_WRITE_NOT_PERMITTED";
    248             case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
    249                 return "GATT_INSUFFICIENT_AUTHENTICATION";
    250             case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED:
    251                 return "GATT_REQUEST_NOT_SUPPORTED";
    252             case BluetoothGatt.GATT_INVALID_OFFSET:
    253                 return "GATT_INVALID_OFFSET";
    254             case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH:
    255                 return "GATT_INVALID_ATTRIBUTE_LENGTH";
    256             case BluetoothGatt.GATT_CONNECTION_CONGESTED:
    257                 return "GATT_CONNECTION_CONGESTED";
    258             default:
    259                 return "unknown";
    260         }
    261     }
    262 
    263     private ScanCallback mScanCallback = new ScanCallback() {
    264         @Override
    265         public void onScanResult(int callbackType, ScanResult result) {
    266             BluetoothDevice device = result.getDevice();
    267             if (Log.isLoggable(TAG, Log.DEBUG)) {
    268                 Log.d(TAG, "Scan result found: " + result.getScanRecord().getServiceUuids());
    269             }
    270 
    271             if (!hasServiceUuid(result)) {
    272                 return;
    273             }
    274 
    275             for (ParcelUuid uuid : result.getScanRecord().getServiceUuids()) {
    276                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    277                     Log.d(TAG, "Scan result UUID: " + uuid);
    278                 }
    279                 if (uuid.equals(mServiceUuid)) {
    280                     // This client only supports connecting to one service.
    281                     // Once we find one, stop scanning and open a GATT connection to the device.
    282                     mScanner.stopScan(mScanCallback);
    283                     mBtGatt = device.connectGatt(mContext, false /* autoConnect */, mGattCallback);
    284                     return;
    285                 }
    286             }
    287         }
    288 
    289         @Override
    290         public void onBatchScanResults(List<ScanResult> results) {
    291             for (ScanResult r : results) {
    292                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    293                     Log.d(TAG, "Batch scanResult: " + r.getDevice().getName()
    294                             + " " + r.getDevice().getAddress());
    295                     }
    296             }
    297         }
    298 
    299         @Override
    300         public void onScanFailed(int errorCode) {
    301             Log.w(TAG, "Scan failed: " + errorCode);
    302         }
    303     };
    304 
    305     private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
    306         @Override
    307         public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    308             super.onConnectionStateChange(gatt, status, newState);
    309 
    310             String state = "";
    311 
    312             if (newState == BluetoothProfile.STATE_CONNECTED) {
    313                 state = "Connected";
    314                 mBtGatt.discoverServices();
    315                 for (ClientCallback callback : mCallbacks) {
    316                     callback.onDeviceConnected(gatt.getDevice());
    317                 }
    318 
    319             } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
    320                 state = "Disconnected";
    321                 for (ClientCallback callback : mCallbacks) {
    322                     callback.onDeviceDisconnected();
    323                 }
    324             }
    325             if (Log.isLoggable(TAG, Log.DEBUG)) {
    326                 Log.d(TAG, " Gatt connection status: " + getStatus(status) + " newState: " + state);
    327             }
    328         }
    329 
    330         @Override
    331         public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    332             super.onServicesDiscovered(gatt, status);
    333             if (Log.isLoggable(TAG, Log.DEBUG)) {
    334                 Log.d(TAG, "onServicesDiscovered: " + status);
    335             }
    336 
    337             List<BluetoothGattService> services = gatt.getServices();
    338             if (services == null || services.size() <= 0) {
    339                 return;
    340             }
    341 
    342             // Notify clients of newly discovered services.
    343             for (BluetoothGattService service : mBtGatt.getServices()) {
    344                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    345                     Log.d(TAG, "Found service: " + service.getUuid() + " notifying clients");
    346                 }
    347                 for (ClientCallback callback : mCallbacks) {
    348                     callback.onServiceDiscovered(service);
    349                 }
    350             }
    351         }
    352 
    353         @Override
    354         public void onCharacteristicWrite(BluetoothGatt gatt,
    355                 BluetoothGattCharacteristic characteristic, int status) {
    356             if (Log.isLoggable(TAG, Log.DEBUG)) {
    357                 Log.d(TAG, "onCharacteristicWrite: " + status);
    358             }
    359             processNextAction();
    360         }
    361 
    362         @Override
    363         public void onCharacteristicRead(BluetoothGatt gatt,
    364                 BluetoothGattCharacteristic characteristic, int status) {
    365             if (Log.isLoggable(TAG, Log.DEBUG)) {
    366                 Log.d(TAG, "onCharacteristicRead:" + new String(characteristic.getValue()));
    367             }
    368             processNextAction();
    369         }
    370 
    371         @Override
    372         public void onCharacteristicChanged(BluetoothGatt gatt,
    373                 BluetoothGattCharacteristic characteristic) {
    374             for (ClientCallback callback : mCallbacks) {
    375                 callback.onCharacteristicChanged(gatt, characteristic);
    376             }
    377             processNextAction();
    378         }
    379     };
    380 }
    381