package io.trygvis.android.bt; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.util.Log; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Queue; import io.trygvis.android.F2; import io.trygvis.android.F3; import io.trygvis.android.Function; import static io.trygvis.android.bt.BtPromise.EventType.onCharacteristicChanged; import static io.trygvis.android.bt.BtPromise.EventType.onCharacteristicRead; import static io.trygvis.android.bt.BtPromise.EventType.onCharacteristicWrite; import static io.trygvis.android.bt.BtPromise.EventType.onConnectionStateChange; import static io.trygvis.android.bt.BtPromise.EventType.onDescriptorRead; import static io.trygvis.android.bt.BtPromise.EventType.onDescriptorWrite; import static io.trygvis.android.bt.BtPromise.EventType.onDirect; import static io.trygvis.android.bt.BtPromise.EventType.onFailure; import static io.trygvis.android.bt.BtPromise.EventType.onFinally; import static io.trygvis.android.bt.BtPromise.EventType.onReliableWriteCompleted; import static io.trygvis.android.bt.BtPromise.EventType.onServicesDiscovered; public class BtPromise { private final static String TAG = BtPromise.class.getSimpleName(); private final List actionQ = new ArrayList<>(); private final Queue failureQ = new ArrayDeque<>(); private final Queue finallyQ = new ArrayDeque<>(); private static final PromiseResult waitForNextEvent = new WaitForNextEvent(); private static final PromiseResult stop = new Stop(); private Boolean stopOnFailure; public static class PromiseResult { private PromiseResult() { } public static PromiseResult waitForNextEvent() { return waitForNextEvent; } public static PromiseResult continueDirectly(Object value) { return new ContinueDirectly(value); } public static PromiseResult stop() { return stop; } public static PromiseResult detour(BtPromise promise) { return new Detour(promise); } } private static class WaitForNextEvent extends PromiseResult { } private static class ContinueDirectly extends PromiseResult { private final Object value; private ContinueDirectly(Object value) { this.value = value; } } private static class Stop extends PromiseResult { } private static class Detour extends PromiseResult { final BtPromise promise; private Detour(BtPromise promise) { this.promise = promise; } } private boolean stopOnFailure() { if(stopOnFailure != null) { boolean b = stopOnFailure; stopOnFailure = null; return b; } return false; } public BtPromise ignoreFailureForNext() { stopOnFailure = true; return this; } public synchronized BtPromise onConnectionStateChange(F3 callback) { actionQ.add(new BtCallback(stopOnFailure(), "onConnectionStateChange") { @Override public PromiseResult onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { return callback.apply(gatt, status,newState); } }); return this; } public synchronized BtPromise onServicesDiscovered(Function callback) { actionQ.add(new BtCallback(stopOnFailure(), "onServicesDiscovered") { @Override public PromiseResult onServicesDiscovered(BluetoothGatt gatt) { return callback.apply(gatt); } }); return this; } public synchronized BtPromise onCharacteristicRead(F2 callback) { actionQ.add(new BtCallback(stopOnFailure(), "onCharacteristicRead") { @Override public PromiseResult onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { return callback.apply(gatt, characteristic); } }); return this; } public synchronized BtPromise onCharacteristicWrite(F2 callback) { actionQ.add(new BtCallback(stopOnFailure(), "onCharacteristicWrite") { @Override public PromiseResult onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { return callback.apply(gatt, characteristic); } }); return this; } public synchronized BtPromise onCharacteristicChanged(F2 callback) { actionQ.add(new BtCallback(stopOnFailure(), "onCharacteristicChanged") { @Override public PromiseResult onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { return callback.apply(gatt, characteristic); } }); return this; } public synchronized BtPromise onDescriptorRead(F2 callback) { actionQ.add(new BtCallback(stopOnFailure(), "onDescriptorRead") { @Override public PromiseResult onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor) { return callback.apply(gatt, descriptor); } }); return this; } public synchronized BtPromise onDescriptorWrite(F2 callback) { actionQ.add(new BtCallback(stopOnFailure(), "onDescriptorWrite") { @Override public PromiseResult onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor) { return callback.apply(gatt, descriptor); } }); return this; } public synchronized BtPromise onReliableWriteCompleted(Function callback) { actionQ.add(new BtCallback(stopOnFailure(), "onReliableWriteCompleted") { @Override public PromiseResult onReliableWriteCompleted(BluetoothGatt gatt) { return callback.apply(gatt); } }); return this; } public synchronized BtPromise onReadRemoteRssi(F2 callback) { actionQ.add(new BtCallback(stopOnFailure(), "onReadRemoteRssi") { @Override public PromiseResult onReadRemoteRssi(BluetoothGatt gatt, int rssi) { return callback.apply(gatt, rssi); } }); return this; } public synchronized BtPromise onDirect(Function callback) { actionQ.add(new BtCallback(stopOnFailure(), "onDirect") { @Override public PromiseResult onDirect(Object value) { return callback.apply(value); } }); return this; } public synchronized BtPromise onFailure(Runnable callback) { failureQ.add(new BtCallback(stopOnFailure(), "onFailure") { @Override public void onFailure() { callback.run(); } }); return this; } public synchronized BtPromise onFinally(Runnable callback) { finallyQ.add(new BtCallback(stopOnFailure(), "finally") { @Override public void onFinally() { callback.run(); } }); return this; } public String toString() { StringBuilder s = new StringBuilder("Queue: "); Iterator it = actionQ.iterator(); int i = 0; while (it.hasNext()) { BtCallback c = it.next(); if (i > 0) { s.append(", "); } s.append(c.name); i++; } return s.toString(); } BtBluetoothGattCallback asCallback(String address) { return new BtBluetoothGattCallback(address, actionQ, failureQ, finallyQ); } enum EventType { onConnectionStateChange, onServicesDiscovered, onCharacteristicRead, onCharacteristicWrite, onCharacteristicChanged, onDescriptorRead, onDescriptorWrite, onReliableWriteCompleted, onReadRemoteRssi, onDirect, onFailure, onFinally, } private static PromiseResult callCallback(EventType key, BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor, int status, int newState, BtCallback btCallback, Object value) { switch (key) { case onConnectionStateChange: return btCallback.onConnectionStateChange(gatt, status, newState); case onServicesDiscovered: return btCallback.onServicesDiscovered(gatt); case onCharacteristicRead: return btCallback.onCharacteristicRead(gatt, characteristic); case onCharacteristicWrite: return btCallback.onCharacteristicWrite(gatt, characteristic); case onCharacteristicChanged: return btCallback.onCharacteristicChanged(gatt, characteristic); case onDescriptorRead: return btCallback.onDescriptorRead(gatt, descriptor); case onDescriptorWrite: return btCallback.onDescriptorWrite(gatt, descriptor); case onReliableWriteCompleted: return btCallback.onReliableWriteCompleted(gatt); case onDirect: return btCallback.onDirect(value); case onFailure: btCallback.onFailure(); return null; case onFinally: btCallback.onFinally(); return null; default: Log.w(TAG, "Unknown callback: " + key); return null; } } static class BtBluetoothGattCallback extends BluetoothGattCallback { private final String address; private List actionQ; private ListIterator it; private ArrayDeque failureQ; ArrayDeque finallyQ; private List events = new ArrayList<>(); private BtBluetoothGattCallback(String address, List actionQ, Queue failureQ, Queue finallyQ) { this.address = address; this.actionQ = new ArrayList<>(actionQ); this.failureQ = new ArrayDeque<>(failureQ); this.finallyQ = new ArrayDeque<>(finallyQ); this.it = actionQ.listIterator(); } void onEvent(EventType key, String values, BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor, int status, int newState, Object value) { boolean success = status == BluetoothGatt.GATT_SUCCESS; events.add(key + "(" + values + "), success=" + success); Log.i(TAG, "event: " + key + "(" + values + "), success=" + success); if (!success) { doFailure(); return; } BtCallback btCallback; synchronized (this) { if (!it.hasNext()) { Log.d(TAG, "All Bluetooth actions are done"); doFinally(); return; } btCallback = it.next(); Log.i(TAG, "Executing bt action: " + btCallback.name); try { Thread.sleep(1000); } catch (InterruptedException e) { // ignore } } try { PromiseResult result = callCallback(key, gatt, characteristic, descriptor, status, newState, btCallback, value); if (result instanceof Stop) { Log.i(TAG, "The chain want to stop."); doFinally(); } else if (result instanceof Detour) { Log.i(TAG, "Adding detour"); BtPromise promise = ((Detour) result).promise; events.add("detour " + promise.actionQ.size()); if (!promise.failureQ.isEmpty()) { Log.i(TAG, "Ignoring " + promise.failureQ.size() + " items from the failure Q"); } if (!promise.finallyQ.isEmpty()) { Log.i(TAG, "Ignoring " + promise.finallyQ.size() + " items from the finally Q"); } synchronized (this) { for (BtCallback cb : promise.actionQ) { it.add(cb); } } } else if (result instanceof ContinueDirectly) { value = ((ContinueDirectly) result).value; onEvent(onDirect, "value=" + value, null, null, null, BluetoothGatt.GATT_SUCCESS, 0, value); return; } if (!it.hasNext()) { Log.i(TAG, "The queue is empty"); } } catch (NotOverriddenException e) { Log.w(TAG, "Unexpected callback by listener: " + key); doFailure(); } } private void doFailure() { StringBuilder msg = new StringBuilder(); msg.append("Address: ").append(address).append("\n"); msg.append("Expected events: \n"); for (BtCallback cb : actionQ) { msg.append("- ").append(cb.name).append("\n"); } msg.append("Actual events: \n"); for (String event : events) { msg.append("- ").append(event).append("\n"); } Log.w(TAG, msg.toString()); Log.w(TAG, "Executing " + failureQ.size() + " failure handlers"); for (BtCallback callback = failureQ.poll(); callback != null; callback = failureQ.poll()) { try { callCallback(onFailure, null, null, null, 0, 0, callback, null); } catch (NotOverriddenException e) { return; } } doFinally(); } private void doFinally() { actionQ.clear(); Log.w(TAG, "Executing " + finallyQ.size() + " finally handlers"); for (BtCallback callback = finallyQ.poll(); callback != null; callback = finallyQ.poll()) { try { callCallback(onFinally, null, null, null, 0, 0, callback, null); } catch (NotOverriddenException e) { return; } } } // ----------------------------------------------------------------------- // // ----------------------------------------------------------------------- @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { onEvent(onConnectionStateChange, "status=" + status + ", newState=" + newState, gatt, null, null, status, newState, null); } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { onEvent(onServicesDiscovered, "status=" + status, gatt, null, null, status, 9, null); } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { onEvent(onCharacteristicRead, "status=" + status + ", characteristic=" + characteristic.getUuid(), gatt, characteristic, null, status, 0, null); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { onEvent(onCharacteristicWrite, "status=" + status + ", characteristic=" + characteristic.getUuid(), gatt, characteristic, null, status, 0, null); } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { onEvent(onCharacteristicChanged, "characteristic=" + characteristic.getUuid(), gatt, characteristic, null, BluetoothGatt.GATT_SUCCESS, 0, null); } @Override public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { onEvent(onDescriptorRead, "status=" + status + ", descriptor=" + descriptor.getUuid(), gatt, null, descriptor, status, 0, null); } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { onEvent(onDescriptorWrite, "status=" + status + ", descriptor=" + descriptor.getUuid(), gatt, null, descriptor, status, 0, null); } @Override public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { onEvent(onReliableWriteCompleted, "status=" + status, gatt, null, null, status, 0, null); } @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { // onEvent(onReadRemoteRssi, "status=" + status, gatt, null, null, status, 0); } } }