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        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