/* * 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.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattService; import android.os.Handler; import android.support.annotation.VisibleForTesting; import java.io.IOException; import java.util.UUID; import si.inova.neatle.monitor.Connection; import si.inova.neatle.source.AsyncInputSource; import si.inova.neatle.source.InputSource; import si.inova.neatle.util.NeatleLogger; class WriteCommand extends SingleCharacteristicsCommand { private BluetoothGatt gatt; private BluetoothGattCharacteristic writeCharacteristic; private final int writeType; private final InputSource buffer; private final Handler handler = new Handler(); private final Object bufferReadLock = new Object(); private final boolean asyncMode; @VisibleForTesting Thread readerThread; WriteCommand(UUID serviceUUID, UUID characteristicsUUID, int writeType, InputSource buffer, CommandObserver observer) { super(serviceUUID, characteristicsUUID, observer); this.buffer = buffer; this.writeType = writeType; this.asyncMode = buffer instanceof AsyncInputSource; } @Override protected void start(Connection connection, BluetoothGatt gatt) { BluetoothGattService service = gatt.getService(serviceUUID); if (service == null) { NeatleLogger.i("Service for write not found [" + serviceUUID + "]"); finish(CommandResult.createErrorResult(characteristicUUID, BluetoothGatt.GATT_FAILURE)); return; } writeCharacteristic = service.getCharacteristic(characteristicUUID); if (writeCharacteristic == null) { NeatleLogger.i("Characteristic not found [" + characteristicUUID + "]"); finish(CommandResult.createErrorResult(characteristicUUID, BluetoothGatt.GATT_FAILURE)); return; } writeCharacteristic.setWriteType(writeType); this.gatt = gatt; if (asyncMode) { readerThread = new Thread(new AsyncReader()); readerThread.start(); } else { try { buffer.open(); nextChunkReady(buffer.nextChunk()); } catch (IOException ex) { NeatleLogger.e("Failed to read from the input source", ex); finish(CommandResult.createErrorResult(characteristicUUID, BluetoothGatt.GATT_FAILURE)); } } } @Override protected void onFinished(CommandResult result) { super.onFinished(result); if (readerThread != null) { readerThread.interrupt(); } NeatleLogger.d("Writing finished [" + characteristicUUID + "]"); } private void nextChunkReady(byte[] chunk) { if (chunk == null) { finish(CommandResult.createEmptySuccess(characteristicUUID)); return; } NeatleLogger.d("Writing " + chunk.length + " bytes onto " + writeCharacteristic.getUuid()); writeCharacteristic.setValue(chunk); if (!gatt.writeCharacteristic(writeCharacteristic)) { NeatleLogger.d("Write returned false"); finish(CommandResult.createErrorResult(characteristicUUID, BluetoothGatt.GATT_FAILURE)); } } @Override protected void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status != BluetoothGatt.GATT_SUCCESS) { NeatleLogger.i("Write on " + characteristic.getUuid() + " failed with status " + status); finish(CommandResult.createErrorResult(characteristicUUID, status)); return; } if (asyncMode) { synchronized (bufferReadLock) { bufferReadLock.notify(); } } else { byte[] chunk; try { chunk = buffer.nextChunk(); } catch (IOException ex) { NeatleLogger.e("Failed to get the first chunk", ex); finish(CommandResult.createErrorResult(characteristicUUID, BluetoothGatt.GATT_FAILURE)); try { buffer.close(); } catch (IOException e) { NeatleLogger.e("Failed to close input source", e); } return; } if (chunk == null) { try { buffer.close(); } catch (IOException e) { NeatleLogger.e("Failed to close input source", e); } finish(CommandResult.createEmptySuccess(characteristicUUID)); return; } nextChunkReady(chunk); } } @Override protected void onError(int error) { NeatleLogger.e("Unexpected error while writing [" + error + "]"); finish(CommandResult.createErrorResult(characteristicUUID, error)); if (asyncMode && readerThread != null) { readerThread.interrupt(); } } @Override public String toString() { return "WriteCommand[async:" + asyncMode + " - " + characteristicUUID + "] on [" + serviceUUID + "]"; } private class AsyncReader implements Runnable { public void run() { try { buffer.open(); } catch (IOException io) { fail(io); return; } try { while (!Thread.interrupted()) { final byte[] chunk = buffer.nextChunk(); handler.post(new Runnable() { @Override public void run() { nextChunkReady(chunk); } }); if (chunk == null) { return; } synchronized (bufferReadLock) { bufferReadLock.wait(); } } } catch (InterruptedException ignored) { // We got interrupted. Error was already handled elsewhere. } catch (IOException ex) { fail(ex); } finally { try { buffer.close(); } catch (IOException closeEx) { NeatleLogger.e("Failed to close input source", closeEx); } } } private void fail(Exception ex) { NeatleLogger.e("Failed to read", ex); finish(CommandResult.createErrorResult(characteristicUUID, BluetoothGatt.GATT_FAILURE)); } } }