/* Copyright (C) 2020-2024 Daniel Dakhno This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file; import android.bluetooth.BluetoothGattCharacteristic; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.UUID; import java.util.zip.CRC32; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.file.FileHandle; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.ResultCode; public class FilePutRawRequest extends FossilRequest { public enum UploadState {INITIALIZED, UPLOADING, CLOSING, UPLOADED} public UploadState state; public ArrayList packets = new ArrayList<>(); private short handle; private FossilWatchAdapter adapter; byte[] file; int fullCRC; public FilePutRawRequest(short handle, byte[] file, FossilWatchAdapter adapter) { this.handle = handle; this.adapter = adapter; int fileLength = file.length; ByteBuffer buffer = this.createBuffer(); buffer.putShort(1, handle); buffer.putInt(3, 0); buffer.putInt(7, fileLength); buffer.putInt(11, fileLength); this.data = buffer.array(); this.file = file; state = UploadState.INITIALIZED; } public FilePutRawRequest(FileHandle handle, byte[] file, FossilWatchAdapter adapter) { this(handle.getHandle(), file, adapter); } public short getHandle() { return handle; } @Override public void handleResponse(BluetoothGattCharacteristic characteristic) { byte[] value = characteristic.getValue(); if (characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")) { int responseType = value[0] & 0x0F; log("response: " + responseType); switch (responseType) { case 3: { if (value.length != 5 || (value[0] & 0x0F) != 3) { throw new RuntimeException("wrong answer header"); } state = UploadState.UPLOADING; TransactionBuilder transactionBuilder = new TransactionBuilder("file upload"); BluetoothGattCharacteristic uploadCharacteristic = adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0004-957f-7d4a-34a6-74696673696d")); this.prepareFilePackets(this.file); for (int i = 0, packetCount = packets.size(); i < packetCount; i++) { byte[] packet = packets.get(i); transactionBuilder.write(uploadCharacteristic, packet); onPacketWritten(transactionBuilder, i, packetCount); } transactionBuilder.queue(adapter.getDeviceSupport().getQueue()); break; } case 8: { if (value.length == 4) return; ByteBuffer buffer = ByteBuffer.wrap(value); buffer.order(ByteOrder.LITTLE_ENDIAN); short handle = buffer.getShort(1); int crc = buffer.getInt(8); byte status = value[3]; ResultCode code = ResultCode.fromCode(status); if(!code.inidicatesSuccess()){ throw new RuntimeException("upload status: " + code + " (" + status + ")"); } if (handle != this.handle) { throw new RuntimeException("wrong response handle"); } if (crc != this.fullCRC) { throw new RuntimeException("file upload exception: wrong crc"); } ByteBuffer buffer2 = ByteBuffer.allocate(3); buffer2.order(ByteOrder.LITTLE_ENDIAN); buffer2.put((byte) 4); buffer2.putShort(this.handle); new TransactionBuilder("file close") .write( adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d")), buffer2.array() ) .queue(adapter.getDeviceSupport().getQueue()); this.state = UploadState.CLOSING; break; } case 4: { if (value.length == 9) return; if (value.length != 4 || (value[0] & 0x0F) != 4) { throw new RuntimeException("wrong file closing header"); } ByteBuffer buffer = ByteBuffer.wrap(value); buffer.order(ByteOrder.LITTLE_ENDIAN); short handle = buffer.getShort(1); if (handle != this.handle) { onFilePut(false); throw new RuntimeException("wrong file closing handle"); } byte status = buffer.get(3); ResultCode code = ResultCode.fromCode(status); if(!code.inidicatesSuccess()){ onFilePut(false); throw new RuntimeException("wrong closing status: " + code + " (" + status + ")"); } this.state = UploadState.UPLOADED; onFilePut(true); log("uploaded file"); break; } case 9: { this.onFilePut(false); throw new RuntimeException("file put timeout"); /*timeout = true; ByteBuffer buffer2 = ByteBuffer.allocate(3); buffer2.order(ByteOrder.LITTLE_ENDIAN); buffer2.put((byte) 4); buffer2.putShort(this.handle); new TransactionBuilder("file close") .write( adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d")), buffer2.array() ) .queue(adapter.getDeviceSupport().getQueue()); this.state = UploadState.CLOSING; break;*/ } } } } @Override public boolean isFinished() { return this.state == UploadState.UPLOADED; } private void prepareFilePackets(byte[] file) { int maxPacketSize = adapter.getMTU() - 4; byte[] data = file; CRC32 fullCRC = new CRC32(); fullCRC.update(data); this.fullCRC = (int) fullCRC.getValue(); int packetCount = (int) Math.ceil(data.length / (float) maxPacketSize); for (int i = 0; i < packetCount; i++) { int currentPacketLength = Math.min(maxPacketSize, data.length - i * maxPacketSize); byte[] packet = new byte[currentPacketLength + 1]; packet[0] = (byte) i; System.arraycopy(data, i * maxPacketSize, packet, 1, currentPacketLength); packets.add(packet); } } public void onFilePut(boolean success) { } public void onPacketWritten(TransactionBuilder transactionBuilder, int packetNr, int packetCount) { } @Override public byte[] getStartSequence() { return new byte[]{0x03}; } @Override public int getPayloadLength() { return 15; } @Override public UUID getRequestUUID() { return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d"); } }