/*
* 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.operation;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.content.Context;
import android.os.Handler;
import android.support.annotation.RestrictTo;
import java.util.Collection;
import java.util.LinkedList;
import si.inova.neatle.Device;
import si.inova.neatle.monitor.Connection;
import si.inova.neatle.util.DeviceManager;
import si.inova.neatle.util.NeatleLogger;
class OperationImpl implements Operation {
private static Command EMPTY_COMMAND = new EmptyCommand();
private final Context context;
private final LinkedList<Command> commands;
private LinkedList<Command> commandQueue;
private Command currentCommand = EMPTY_COMMAND;
private CommandResult lastResult;
private final OperationObserver operationObserver;
private OperationResults results;
private final BluetoothDevice device;
private Device connection;
private final CommandHandler commandHandler = new CommandHandler();
private final Handler handler = new Handler();
private final GattCallback callback = new GattCallback();
private BluetoothGatt gatt;
private final int retryCount;
private int retriedCount = 0;
private boolean yielded;
private boolean canceled = false;
OperationImpl(Context context, BluetoothDevice device, Collection<Command> commands, int retryCount, OperationObserver operationObserver) {
this.context = context;
this.device = device;
this.commands = new LinkedList<>(commands);
this.commandQueue = new LinkedList<>(commands);
this.retryCount = retryCount;
this.operationObserver = operationObserver;
}
@Override
public void execute() {
if (connection != null) {
return;
}
Device conn;
synchronized (this) {
this.connection = conn = DeviceManager.getInstance(context).getDevice(device);
this.results = new OperationResults();
this.commandQueue = new LinkedList<>(commands);
this.currentCommand = EMPTY_COMMAND;
this.retriedCount = 0;
this.canceled = false;
this.lastResult = null;
}
conn.execute(callback);
}
private void retry() {
Device conn;
synchronized (this) {
retriedCount++;
NeatleLogger.i("Retrying operation, attempt:" + retriedCount);
results = new OperationResults();
commandQueue = new LinkedList<>(commands);
conn = connection;
lastResult = null;
}
conn.execute(callback);
}
@Override
public void cancel() {
if (connection == null) {
return;
}
synchronized (this) {
canceled = true;
}
done();
}
@Override
public synchronized boolean isCanceled() {
return canceled;
}
private void done() {
Device conn;
boolean wasExecuting;
synchronized (this) {
wasExecuting = this.connection != null;
conn = this.connection;
this.connection = null;
}
if (wasExecuting) {
conn.executeFinished(callback);
NeatleLogger.d("Operation finished, success: " + results.wasSuccessful() + ", cancel:" + isCanceled());
handler.post(new Runnable() {
@Override
public void run() {
if (isCanceled()) {
return;
}
if (operationObserver != null) {
operationObserver.onOperationFinished(OperationImpl.this, results);
}
}
});
}
}
private void executeNext() {
Command cmd;
Device targetDevice;
synchronized (this) {
targetDevice = connection;
if (yielded) {
return;
}
if (lastResult != null && !lastResult.wasSuccessful()) {
if (retryCount == -1 || retriedCount + 1 <= retryCount) {
retry();
return;
}
NeatleLogger.i("Command failed. Aborting operation. Error: " + lastResult.getStatus());
done();
return;
}
Command old = currentCommand;
currentCommand = commandQueue.poll();
NeatleLogger.d("Continuing with " + currentCommand + " after " + old + " with " + lastResult);
if (currentCommand == null) {
currentCommand = EMPTY_COMMAND;
done();
return;
}
cmd = currentCommand;
}
NeatleLogger.d("Executing command: " + currentCommand);
if (operationObserver != null) {
operationObserver.onCommandStarted(this, cmd);
}
cmd.execute(targetDevice, commandHandler, gatt);
}
private void scheduleNext() {
NeatleLogger.d("Scheduling next command after : " + currentCommand);
handler.post(new Runnable() {
@Override
public void run() {
executeNext();
}
});
}
@RestrictTo(RestrictTo.Scope.TESTS)
LinkedList<Command> getCommands() {
return commands;
}
@RestrictTo(RestrictTo.Scope.TESTS)
OperationObserver getOperationObserver() {
return operationObserver;
}
@RestrictTo(RestrictTo.Scope.TESTS)
int getRetryCount() {
return retryCount;
}
@RestrictTo(RestrictTo.Scope.TESTS)
BluetoothDevice getDevice() {
return device;
}
@Override
public String toString() {
return "Operation[retryCount: " + retryCount + ", attempts: " + retriedCount + ", commands:" + this.commands + "]";
}
private static class EmptyCommand extends Command {
private EmptyCommand() {
super(null, null, null);
}
@Override
protected void execute(Connection connection, CommandObserver observer, BluetoothGatt gatt) {
super.execute(connection, observer, gatt);
throw new IllegalStateException("Should not be called");
}
@Override
protected void onError(int error) {
}
}
private class CommandHandler implements CommandObserver {
@Override
public void finished(final Command command, final CommandResult result) {
synchronized (OperationImpl.this) {
lastResult = result;
results.addResult(result);
//once the command is finished, don't forward any more events
currentCommand = EMPTY_COMMAND;
}
NeatleLogger.d("Command finished, status: " + result.getStatus() + ", command:" + command + ", on: " + device.getAddress());
handler.post(new Runnable() {
@Override
public void run() {
if (operationObserver != null) {
if (result.wasSuccessful()) {
operationObserver.onCommandSuccess(OperationImpl.this, command, result);
} else {
operationObserver.onCommandError(OperationImpl.this, command, result.getStatus());
}
}
}
});
scheduleNext();
}
}
private class GattCallback extends BluetoothGattCallback {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
NeatleLogger.i("OperationImpl: onConnectionStateChange, state:" + status + ", newState: " + newState);
Command cur;
synchronized (OperationImpl.this) {
cur = currentCommand;
if (newState != BluetoothGatt.STATE_CONNECTED && cur == EMPTY_COMMAND &&
(lastResult == null || lastResult.wasSuccessful())) {
lastResult = CommandResult.createErrorResult(null, BluetoothGatt.GATT_FAILURE);
scheduleNext();
return;
}
}
cur.onConnectionStateChange(gatt, status, newState);
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
synchronized (OperationImpl.this) {
yielded = false;
OperationImpl.this.gatt = gatt;
}
scheduleNext();
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
currentCommand.onCharacteristicRead(gatt, characteristic, status);
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
currentCommand.onCharacteristicWrite(gatt, characteristic, status);
}
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
currentCommand.onDescriptorRead(gatt, descriptor, status);
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
currentCommand.onDescriptorWrite(gatt, descriptor, status);
}
@Override
public String toString() {
return "Callback[" + OperationImpl.this.toString() + "]";
}
}
}