SimpleBleClient.java revision 77e5e49cf9dcceb69b07510c380ae2a9285ebfee
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 */
16package com.android.car.trust.comms;
17
18import android.bluetooth.BluetoothDevice;
19import android.bluetooth.BluetoothGatt;
20import android.bluetooth.BluetoothGattCallback;
21import android.bluetooth.BluetoothGattCharacteristic;
22import android.bluetooth.BluetoothGattService;
23import android.bluetooth.BluetoothManager;
24import android.bluetooth.BluetoothProfile;
25import android.bluetooth.le.BluetoothLeScanner;
26import android.bluetooth.le.ScanCallback;
27import android.bluetooth.le.ScanFilter;
28import android.bluetooth.le.ScanResult;
29import android.bluetooth.le.ScanSettings;
30import android.content.Context;
31import android.os.Handler;
32import android.os.ParcelUuid;
33import android.support.annotation.NonNull;
34import android.util.Log;
35
36import java.util.ArrayList;
37import java.util.List;
38import java.util.Queue;
39import 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 */
45public 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        Log.d(TAG, "Start scanning for uuid: " + mServiceUuid.getUuid());
133        mScanner.startScan(filters, settings.build(), mScanCallback);
134
135        Handler handler = new Handler();
136        handler.postDelayed(new Runnable() {
137            @Override
138            public void run() {
139                mScanner.stopScan(mScanCallback);
140                Log.d(TAG, "Stopping Scanner");
141            }
142        }, SCAN_TIME_MS);
143    }
144
145    private boolean hasServiceUuid(ScanResult result) {
146        if (result.getScanRecord() == null
147                || result.getScanRecord().getServiceUuids() == null
148                || result.getScanRecord().getServiceUuids().size() == 0) {
149            return false;
150        }
151        return true;
152    }
153
154    /**
155     * Writes to a {@link BluetoothGattCharacteristic} if possible, or queues the action until
156     * other actions are complete.
157     *
158     * @param characteristic {@link BluetoothGattCharacteristic} to be written
159     */
160    public void writeCharacteristic(BluetoothGattCharacteristic characteristic) {
161        processAction(new BleAction(characteristic, BleAction.ACTION_WRITE));
162    }
163
164    /**
165     * Reads a {@link BluetoothGattCharacteristic} if possible, or queues the read action until
166     * other actions are complete.
167     *
168     * @param characteristic {@link BluetoothGattCharacteristic} to be read.
169     */
170    public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
171        processAction(new BleAction(characteristic, BleAction.ACTION_READ));
172    }
173
174    /**
175     * Enable or disable notification for specified {@link BluetoothGattCharacteristic}.
176     *
177     * @param characteristic The {@link BluetoothGattCharacteristic} for which to enable
178     *                       notifications.
179     * @param enabled        True if notifications should be enabled, false otherwise.
180     */
181    public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
182            boolean enabled) {
183        mBtGatt.setCharacteristicNotification(characteristic, enabled);
184    }
185
186    /**
187     * Add a {@link ClientCallback} to listen for updates from BLE components
188     */
189    public void addCallback(ClientCallback callback) {
190        mCallbacks.add(callback);
191    }
192
193    public void removeCallback(ClientCallback callback) {
194        mCallbacks.remove(callback);
195    }
196
197    private void processAction(BleAction action) {
198        // Only execute actions if the queue is empty.
199        if (mBleActionQueue.size() > 0) {
200            mBleActionQueue.add(action);
201            return;
202        }
203
204        mBleActionQueue.add(action);
205        executeAction(mBleActionQueue.peek());
206    }
207
208    private void processNextAction() {
209        mBleActionQueue.poll();
210        executeAction(mBleActionQueue.peek());
211    }
212
213    private void executeAction(BleAction action) {
214        if (action == null) {
215            return;
216        }
217
218        Log.d(TAG, "Executing BLE Action type: " + action.getAction());
219
220        int actionType = action.getAction();
221        switch (actionType) {
222            case BleAction.ACTION_WRITE:
223                mBtGatt.writeCharacteristic(action.getCharacteristic());
224                break;
225            case BleAction.ACTION_READ:
226                mBtGatt.readCharacteristic(action.getCharacteristic());
227                break;
228            default:
229        }
230    }
231
232    private String getStatus(int status) {
233        switch (status) {
234            case BluetoothGatt.GATT_FAILURE:
235                return "Failure";
236            case BluetoothGatt.GATT_SUCCESS:
237                return "GATT_SUCCESS";
238            case BluetoothGatt.GATT_READ_NOT_PERMITTED:
239                return "GATT_READ_NOT_PERMITTED";
240            case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
241                return "GATT_WRITE_NOT_PERMITTED";
242            case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
243                return "GATT_INSUFFICIENT_AUTHENTICATION";
244            case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED:
245                return "GATT_REQUEST_NOT_SUPPORTED";
246            case BluetoothGatt.GATT_INVALID_OFFSET:
247                return "GATT_INVALID_OFFSET";
248            case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH:
249                return "GATT_INVALID_ATTRIBUTE_LENGTH";
250            case BluetoothGatt.GATT_CONNECTION_CONGESTED:
251                return "GATT_CONNECTION_CONGESTED";
252            default:
253                return "unknown";
254        }
255    }
256
257    private ScanCallback mScanCallback = new ScanCallback() {
258        @Override
259        public void onScanResult(int callbackType, ScanResult result) {
260            BluetoothDevice device = result.getDevice();
261            Log.d(TAG, "Scan result found: " + result.getScanRecord().getServiceUuids());
262
263            if (!hasServiceUuid(result)) {
264                return;
265            }
266
267            for (ParcelUuid uuid : result.getScanRecord().getServiceUuids()) {
268                Log.d(TAG, "Scan result UUID: " + uuid);
269                if (uuid.equals(mServiceUuid)) {
270                    // This client only supports connecting to one service.
271                    // Once we find one, stop scanning and open a GATT connection to the device.
272                    mScanner.stopScan(mScanCallback);
273                    mBtGatt = device.connectGatt(mContext, false /* autoConnect */, mGattCallback);
274                    return;
275                }
276            }
277        }
278
279        @Override
280        public void onBatchScanResults(List<ScanResult> results) {
281            for (ScanResult r : results) {
282                Log.d(TAG, "Batch scanResult: "
283                        + r.getDevice().getName() + " " + r.getDevice().getAddress());
284            }
285        }
286
287        @Override
288        public void onScanFailed(int errorCode) {
289            Log.d(TAG, "Scan failed: " + errorCode);
290        }
291    };
292
293    private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
294        @Override
295        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
296            super.onConnectionStateChange(gatt, status, newState);
297
298            String state = "";
299
300            if (newState == BluetoothProfile.STATE_CONNECTED) {
301                state = "Connected";
302                mBtGatt.discoverServices();
303                for (ClientCallback callback : mCallbacks) {
304                    callback.onDeviceConnected(gatt.getDevice());
305                }
306
307            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
308                state = "Disconnected";
309                for (ClientCallback callback : mCallbacks) {
310                    callback.onDeviceDisconnected();
311                }
312            }
313            Log.d(TAG, " Gatt connection status: " + getStatus(status) + " newState: " + state);
314        }
315
316        @Override
317        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
318            super.onServicesDiscovered(gatt, status);
319            Log.d(TAG, "onServicesDiscovered: " + status);
320
321            List<BluetoothGattService> services = gatt.getServices();
322            if (services == null || services.size() <= 0) {
323                return;
324            }
325
326            // Notify clients of newly discovered services.
327            for (BluetoothGattService service : mBtGatt.getServices()) {
328                Log.d(TAG, "Found service: " + service.getUuid() + " notifying clients");
329                for (ClientCallback callback : mCallbacks) {
330                    callback.onServiceDiscovered(service);
331                }
332            }
333        }
334
335        @Override
336        public void onCharacteristicWrite(BluetoothGatt gatt,
337                BluetoothGattCharacteristic characteristic, int status) {
338            Log.d(TAG, "onCharacteristicWrite: " + status);
339            processNextAction();
340        }
341
342        @Override
343        public void onCharacteristicRead(BluetoothGatt gatt,
344                BluetoothGattCharacteristic characteristic, int status) {
345            Log.d(TAG, "onCharacteristicRead:" + new String(characteristic.getValue()));
346            processNextAction();
347        }
348
349        @Override
350        public void onCharacteristicChanged(BluetoothGatt gatt,
351                BluetoothGattCharacteristic characteristic) {
352            for (ClientCallback callback : mCallbacks) {
353                callback.onCharacteristicChanged(gatt, characteristic);
354            }
355            processNextAction();
356        }
357    };
358
359}
360