/* * MIT License * * Copyright (c) 2017 Inova IT * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package si.inova.neatle; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.content.Context; import android.os.Build; import android.os.Handler; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; import android.support.annotation.VisibleForTesting; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import si.inova.neatle.monitor.Connection; import si.inova.neatle.monitor.ConnectionHandler; import si.inova.neatle.monitor.ConnectionStateListener; import si.inova.neatle.operation.CharacteristicsChangedListener; import si.inova.neatle.operation.CommandResult; import si.inova.neatle.util.NeatleLogger; @RestrictTo(RestrictTo.Scope.LIBRARY) public class Device implements Connection { private static final long DISCOVER_DEVICE_TIMEOUT = 60 * 1000; private static BluetoothGattCallback DO_NOTHING_CALLBACK = new BluetoothGattCallback() { }; private final BluetoothDevice device; public final Handler handler = new Handler(); private final GattCallback callback = new GattCallback(); private final Object lock = new Object(); private int state; private Context context; private final BluetoothAdapter adapter; private final LinkedList<BluetoothGattCallback> queue = new LinkedList<>(); private BluetoothGattCallback currentCallback = DO_NOTHING_CALLBACK; private boolean serviceDiscovered; private BluetoothGatt gatt; private final CopyOnWriteArrayList<ConnectionHandler> connectionHandlers = new CopyOnWriteArrayList<>(); private final HashMap<UUID, CopyOnWriteArrayList<CharacteristicsChangedListener>> changeListeners = new HashMap<>(); private final CopyOnWriteArrayList<ConnectionStateListener> connectionStateListeners = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<ServicesDiscoveredListener> servicesDiscoveredListeners = new CopyOnWriteArrayList<>(); private BluetoothAdapter.LeScanCallback discoverCallback = new ScanForDeviceCallback(); private Runnable discoverWatchdog = new ScanForDeviceTimeout(); public Device(Context context, BluetoothDevice device, BluetoothAdapter adapter) { this.device = device; this.context = context.getApplicationContext(); this.adapter = adapter; } @Override public void addConnectionHandler(ConnectionHandler handler) { connectionHandlers.add(handler); } @Override public void removeConnectionHandler(ConnectionHandler handler) { connectionHandlers.remove(handler); //check if we now can disconnect on idle disconnectOnIdle(); } @Override public void addConnectionStateListener(ConnectionStateListener connectionStateListener) { connectionStateListeners.add(connectionStateListener); } @Override public void removeConnectionStateListener(ConnectionStateListener connectionStateListener) { connectionStateListeners.remove(connectionStateListener); } public void addServicesDiscoveredListener(ServicesDiscoveredListener listener) { servicesDiscoveredListeners.add(listener); } public void removeServicesDiscoveredListener(ServicesDiscoveredListener listener) { servicesDiscoveredListeners.remove(listener); } @Override public BluetoothDevice getDevice() { return device; } @Override public BluetoothGattService getService(UUID serviceUUID) { synchronized (lock) { return gatt == null ? null : gatt.getService(serviceUUID); } } @Override public List<BluetoothGattService> getServices() { synchronized (lock) { return gatt == null ? null : gatt.getServices(); } } @Override public int getState() { synchronized (lock) { return state; } } @Override public void addCharacteristicsChangedListener(UUID characteristicsUUID, CharacteristicsChangedListener listener) { synchronized (lock) { CopyOnWriteArrayList<CharacteristicsChangedListener> list = changeListeners.get(characteristicsUUID); if (list == null) { list = new CopyOnWriteArrayList<>(); list.add(listener); changeListeners.put(characteristicsUUID, list); } else if (!list.contains(listener)) { list.add(listener); } } } @Override public int getCharacteristicsChangedListenerCount(UUID characteristicsUUID) { synchronized (lock) { CopyOnWriteArrayList<CharacteristicsChangedListener> list = changeListeners.get(characteristicsUUID); return list == null ? 0 : list.size(); } } @Override @SuppressWarnings("PMD.CompareObjectsWithEquals") public void removeCharacteristicsChangedListener(UUID characteristicsUUID, CharacteristicsChangedListener listener) { boolean checkIdle; synchronized (lock) { CopyOnWriteArrayList<CharacteristicsChangedListener> list = changeListeners.get(characteristicsUUID); if (list != null) { list.remove(listener); if (list.isEmpty()) { changeListeners.remove(characteristicsUUID); } } checkIdle = currentCallback == DO_NOTHING_CALLBACK && queue.isEmpty() && queue.isEmpty(); } if (checkIdle) { disconnectOnIdle(); } } private void notifyCharacteristicChange(final CommandResult change) { handler.post(new Runnable() { @Override public void run() { CopyOnWriteArrayList<CharacteristicsChangedListener> list; synchronized (lock) { list = changeListeners.get(change.getUUID()); } if (list == null) { //a command could have enable a notification by it's own return; } for (CharacteristicsChangedListener listener : list) { listener.onCharacteristicChanged(change); } } }); } @SuppressWarnings("PMD.CompareObjectsWithEquals") public void execute(BluetoothGattCallback callback) { NeatleLogger.d("Execute " + callback); boolean wasIdle; synchronized (lock) { wasIdle = currentCallback == DO_NOTHING_CALLBACK; if (currentCallback == callback || queue.contains(callback)) { NeatleLogger.d("Restarting " + callback); } else { NeatleLogger.d("Queueing up " + callback); queue.add(callback); } } if (wasIdle && areServicesDiscovered()) { resume(); } else { connect(); } } @SuppressWarnings("PMD.CompareObjectsWithEquals") private void disconnectOnIdle() { handler.post(new Runnable() { @Override public void run() { boolean keepAlive = false; for (ConnectionHandler handler : connectionHandlers) { int chRet = handler.onConnectionIdle(Device.this); keepAlive = keepAlive || chRet == ConnectionHandler.ON_IDLE_KEEP_ALIVE; } synchronized (lock) { if (!changeListeners.isEmpty()) { NeatleLogger.i("Idle, but subscriptions are keeping the connection alive - listening for notifications/indications"); return; } //check again, in case some scheduled a new operation in the mean time if (currentCallback != DO_NOTHING_CALLBACK || !queue.isEmpty()) { return; } if (keepAlive) { NeatleLogger.i("Idle, but keeping the connection alive - keep alive set"); return; } if (gatt != null) { NeatleLogger.i("Disconnecting on idle"); disconnect(); } } } }); } private void resume() { BluetoothGattCallback target; BluetoothGatt targetGatt; boolean doResume; synchronized (lock) { if (currentCallback == DO_NOTHING_CALLBACK) { BluetoothGattCallback newCallback = queue.poll(); if (newCallback == null) { if (changeListeners.isEmpty()) { disconnectOnIdle(); } return; } currentCallback = newCallback; } target = currentCallback; doResume = areServicesDiscovered(); targetGatt = this.gatt; } if (doResume) { NeatleLogger.i("Resuming with " + target); currentCallback.onServicesDiscovered(targetGatt, BluetoothGatt.GATT_SUCCESS); } else { NeatleLogger.i("Will resume after services are discovered with " + target); connect(); } } public boolean areServicesDiscovered() { synchronized (lock) { return serviceDiscovered && state == BluetoothGatt.STATE_CONNECTED; } } @Override public void disconnect() { NeatleLogger.i("Disconnecting"); stopDiscovery(); BluetoothGatt target; int oldState; synchronized (lock) { target = gatt; gatt = null; this.serviceDiscovered = false; oldState = state; state = BluetoothGatt.STATE_DISCONNECTED; } if (target != null) { target.disconnect(); } notifyConnectionStateChange(oldState, BluetoothGatt.STATE_DISCONNECTED); } private void discoverDevice() { boolean isScanning = false; do { if (adapter == null || adapter.getState() != BluetoothAdapter.STATE_ON) { isScanning = false; break; } //FIXME: Switch to non-deprecated method. if (!adapter.startLeScan(discoverCallback)) { isScanning = true; break; } } while (false); if (isScanning) { NeatleLogger.e("Failed to start device discovery. Failing connection attempt"); connectionFailed(BluetoothGatt.GATT_FAILURE); } else { handler.postDelayed(discoverWatchdog, DISCOVER_DEVICE_TIMEOUT); } } private void deviceDiscovered() { stopDiscovery(); int state = getState(); if (state == BluetoothGatt.STATE_CONNECTING) { NeatleLogger.i("Device discovered. Continuing with connecting"); connectWithGatt(); } else { NeatleLogger.e("Device discovered but no longer connecting"); } } private void stopDiscovery() { if (adapter != null) { adapter.stopLeScan(discoverCallback); } handler.removeCallbacks(discoverWatchdog); } @SuppressWarnings("PMD.CompareObjectsWithEquals") public void executeFinished(BluetoothGattCallback callback) { synchronized (lock) { if (callback == currentCallback) { this.currentCallback = DO_NOTHING_CALLBACK; NeatleLogger.d("Finished " + callback); handler.post(new Runnable() { @Override public void run() { resume(); } }); } else { this.queue.remove(callback); NeatleLogger.d("Removed from queue " + callback); } } } @Override public void connect() { int oldState; int newState; boolean doConnectGatt = false; boolean doDiscovery = false; boolean adapterEnabled = adapter != null && adapter.isEnabled(); synchronized (lock) { if (isConnected() || isConnecting()) { return; } if (this.gatt != null) { throw new IllegalStateException(); } oldState = state; if (!adapterEnabled) { //newState = BluetoothAdapter.STATE_OFF; NeatleLogger.d("BT off. Won't connect to " + device.getName() + "[" + device.getAddress() + "]"); connectionFailed(BluetoothGatt.GATT_FAILURE); return; } else { newState = BluetoothGatt.STATE_CONNECTING; if (device.getType() == BluetoothDevice.DEVICE_TYPE_UNKNOWN) { doDiscovery = true; } else { doConnectGatt = true; } } } //call these methods outside of the lock, to prevent deadlocks if (doConnectGatt) { connectWithGatt(); return; } synchronized (lock) { state = newState; } notifyConnectionStateChange(oldState, newState); if (doDiscovery) { NeatleLogger.d("Device unknown, let's discover it" + device.getName() + "[" + device.getAddress() + "]"); discoverDevice(); } } @VisibleForTesting void connectWithGatt() { int oldState; int newState = BluetoothGatt.STATE_CONNECTING; synchronized (lock) { oldState = state; state = BluetoothGatt.STATE_CONNECTING; } NeatleLogger.d("Connecting with " + device.getName() + "[" + device.getAddress() + "]"); BluetoothGatt gatt = device.connectGatt(context, false, callback); synchronized (lock) { if (state == BluetoothGatt.STATE_DISCONNECTED) { gatt = null; } this.gatt = gatt; if (gatt == null) { state = BluetoothGatt.STATE_DISCONNECTED; newState = BluetoothGatt.STATE_DISCONNECTED; } } notifyConnectionStateChange(oldState, newState); } private void notifyServicesDiscovered() { handler.post(new Runnable() { @Override public void run() { synchronized (Device.this.lock) { //state has changed after we scheduled this runnable if (!areServicesDiscovered()) { NeatleLogger.d("notifyServicesDiscovered expired."); return; } } for (ServicesDiscoveredListener l : servicesDiscoveredListeners) { l.onServicesDiscovered(Device.this); } } }); } private void notifyConnectionStateChange(final int oldState, final int newState) { NeatleLogger.d("notifyConnectionStateChange from " + oldState + " to " + newState); if (oldState == newState) { return; } handler.post(new Runnable() { @Override public void run() { synchronized (Device.this.lock) { //state has changed after we scheduled this runnable if (newState != state) { NeatleLogger.d("notifyConnectionStateChange expired. Was " + oldState + " to " + newState + " but its " + state + " now"); return; } } for (ConnectionStateListener l : connectionStateListeners) { l.onConnectionStateChanged(Device.this, newState); } } }); } private void connectionFailed(int status) { BluetoothGattCallback current; int oldState; int newState; LinkedList<BluetoothGattCallback> queueCopy; BluetoothGatt oldGatt; synchronized (lock) { oldState = state; state = BluetoothGatt.STATE_DISCONNECTED; newState = state; serviceDiscovered = false; current = currentCallback; queueCopy = new LinkedList<>(queue); oldGatt = this.gatt; this.gatt = null; } NeatleLogger.i("Connection attempt failed. Notifying all pending operations"); current.onConnectionStateChange(oldGatt, status, BluetoothGatt.STATE_DISCONNECTED); for (BluetoothGattCallback cb : queueCopy) { cb.onConnectionStateChange(oldGatt, status, BluetoothGatt.STATE_DISCONNECTED); } notifyConnectionStateChange(oldState, newState); } private void connectionSuccess() { int oldState; int newState; synchronized (lock) { serviceDiscovered = false; oldState = state; state = BluetoothGatt.STATE_CONNECTED; newState = state; } notifyConnectionStateChange(oldState, newState); } public boolean isConnecting() { synchronized (lock) { return state == BluetoothGatt.STATE_CONNECTING; } } public boolean isConnected() { synchronized (lock) { return state == BluetoothGatt.STATE_CONNECTED; } } /*public void yield(BluetoothGattCallback from, BluetoothGattCallback to) { synchronized (lock) { if (currentCallback == from) { queue.add(0, currentCallback); currentCallback = to; NeatleLogger.d("Yielded to " + to); } else { throw new IllegalArgumentException("Cannot yield. Operation not in progress"); } } handler.post(new Runnable() { @Override public void run() { resume(); } }); }*/ private class GattCallback extends BluetoothGattCallback { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { boolean didConnect = false; NeatleLogger.d("onConnectionStateChange status: " + status + " newState:" + newState); if (newState == BluetoothGatt.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { didConnect = gatt.discoverServices(); } if (!didConnect) { gatt.close(); connectionFailed(status); } else { connectionSuccess(); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status != BluetoothGatt.GATT_SUCCESS) { connectionFailed(status); return; } synchronized (lock) { serviceDiscovered = true; } notifyServicesDiscovered(); resume(); } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { NeatleLogger.d("createCharacteristicRead"); BluetoothGattCallback target; synchronized (lock) { target = currentCallback; } target.onCharacteristicRead(gatt, characteristic, status); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { NeatleLogger.d("onCharacteristicWrite " + status); BluetoothGattCallback target; synchronized (lock) { target = currentCallback; } target.onCharacteristicWrite(gatt, characteristic, status); } @Override public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { NeatleLogger.d("onReliableWriteCompleted"); BluetoothGattCallback target; synchronized (lock) { target = currentCallback; } target.onReliableWriteCompleted(gatt, status); } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { BluetoothGattCallback target; synchronized (lock) { target = currentCallback; } target.onCharacteristicChanged(gatt, characteristic); notifyCharacteristicChange(CommandResult.createCharacteristicChanged(characteristic)); } @Override public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { BluetoothGattCallback target; synchronized (lock) { target = currentCallback; } target.onDescriptorRead(gatt, descriptor, status); } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { BluetoothGattCallback target; synchronized (lock) { target = currentCallback; } target.onDescriptorWrite(gatt, descriptor, status); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { BluetoothGattCallback target; synchronized (lock) { target = currentCallback; } target.onMtuChanged(gatt, mtu, status); } @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { BluetoothGattCallback target; synchronized (lock) { target = currentCallback; } target.onReadRemoteRssi(gatt, rssi, status); } } private class ScanForDeviceCallback implements BluetoothAdapter.LeScanCallback { @Override public void onLeScan(BluetoothDevice found, int rssi, byte[] scanRecord) { if (found.getAddress().equals(device.getAddress())) { deviceDiscovered(); } } } private class ScanForDeviceTimeout implements Runnable { public void run() { stopDiscovery(); int state = getState(); if (state == BluetoothGatt.STATE_CONNECTING) { NeatleLogger.e("Device no discovered failing connection attempt."); connectionFailed(BluetoothGatt.GATT_FAILURE); } else { NeatleLogger.e("Discover timeout but we are not connecting anymore."); } } } }