diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java index 0b0d893b30..bc10e5bbab 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java @@ -63,4 +63,5 @@ public class PineTimeJFConstants { public static final UUID UUID_SERVICE_HEART_RATE = GattService.UUID_SERVICE_HEART_RATE; public static final UUID UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb"); + public static final String ACTION_UPLOAD_PROGRESS = "PINETIME_UPLOAD_PROGRESS"; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsAction.java deleted file mode 100644 index 7972ca6640..0000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsAction.java +++ /dev/null @@ -1,18 +0,0 @@ -package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.adablefs; - -class AdaBleFsAction { - public enum Method { - UPLOAD, - DELETE, - }; - public String filenameorpath; - public String secondFilenameorpath; - public Method method; - public byte[] data; - - public AdaBleFsAction(AdaBleFsAction.Method method, String filenameorpath, byte[] data) { - this.filenameorpath = filenameorpath; - this.method = method; - this.data = data; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsAction.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsAction.kt new file mode 100644 index 0000000000..5ea7e92f25 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsAction.kt @@ -0,0 +1,36 @@ +/* Copyright (C) 2025 Arjan Schrijver + + 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.btle.profiles.adablefs + +/** + * Represents an action to perform on the Ada BLE filesystem. + * Can be a file upload, delete, or other FS operations. + */ +data class AdaBleFsAction( + val method: Method, + val filenameorpath: String, + val data: ByteArray = ByteArray(0), + val secondFilenameorpath: String = "" +) { + enum class Method { + UPLOAD, + DELETE, + MAKE_DIRECTORY, + MOVE, + LIST_DIRECTORY + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsProfile.java deleted file mode 100644 index fcb4fc2d36..0000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsProfile.java +++ /dev/null @@ -1,559 +0,0 @@ -/* Copyright (C) 2016-2021 Andreas Shimokawa, Carsten Pfeiffer, João - Paulo Barraca, JohnnySun - - 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.btle.profiles.adablefs; - -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.content.Context; -import android.net.Uri; - -import org.json.JSONArray; -import org.json.JSONObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.Vector; - -import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; -import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport; -import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue; -import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; -import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; -import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; -import nodomain.freeyourgadget.gadgetbridge.util.GBZipFile; -import nodomain.freeyourgadget.gadgetbridge.util.RemoteFileSystemCache; -import nodomain.freeyourgadget.gadgetbridge.util.UriHelper; -import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException; - -public class AdaBleFsProfile extends AbstractBleProfile { - final byte PADDING_BYTE = 0x00; - final byte REQUEST_CONTINUED = 0x01; - final byte REQUEST_WRITE_FILE_START = 0x20; - final byte RESPONSE_WRITE_FILE = 0x21; - final byte REQUEST_WRITE_FILE_DATA = 0x22; - final byte REQUEST_DELETE_FILE = 0x30; - final byte RESPONSE_DELETE_FILE = 0x31; - final byte REQUEST_MAKE_DIRECTORY = 0x40; - final byte RESPONSE_MAKE_DIRECTORY = 0x41; - final byte REQUEST_LIST_DIRECTORY = 0x50; - final byte RESPONSE_LIST_DIRECTORY = 0x51; - final byte REQUEST_MOVE_FILE_DIRECTORY = 0x60; - final byte RESPONSE_MOVE_FILE_DIRECTORY = 0x61; - - - final byte STATUS_OK = 0x01; - - private enum TriState {TRUE, FALSE, UNKNOWN}; - - - static final UUID UUID_SERVICE_FS = UUID.fromString("0000febb-0000-1000-8000-00805f9b34fb"); - static final UUID UUID_CHARACTERISTIC_FS_VERSION = UUID.fromString("adaf0100-4669-6c65-5472-616e73666572"); - static final public UUID UUID_CHARACTERISTIC_FS_TRANSFER = UUID.fromString("adaf0200-4669-6c65-5472-616e73666572"); - - private BtLEQueue btleQueue; - private GBDevice device; - - private LinkedList adaBleFsQueue; - int bytesOfFileWritten; - int locationToWriteTo; - int chunkSize; - - private RemoteFileSystemCache remoteFs; - List listingDirectory; - - private static final Logger LOG = LoggerFactory.getLogger(AdaBleFsProfile.class); - - public AdaBleFsProfile(T support) { - super(support); - adaBleFsQueue = new LinkedList<>(); - chunkSize = 20; // Default MTU for android, I believe? - remoteFs = new RemoteFileSystemCache(); - } - - public void loadResources(Uri uri, Context context, BtLEQueue queue) { - this.btleQueue = queue; - // Unzip - try { - UriHelper uriHelper = UriHelper.get(uri, context); - GBZipFile zipPackage = new GBZipFile(uriHelper.openInputStream()); - - JSONObject resources_manifest = new JSONObject(new String(zipPackage.getFileFromZip(("resources.json")))); - JSONArray resources = resources_manifest.getJSONArray("resources"); - for(int completed = 0; completed < resources.length(); completed++) { - JSONObject fileItem = resources.getJSONObject(completed); - adaBleFsQueue.add( - new AdaBleFsAction( - AdaBleFsAction.Method.UPLOAD, - fileItem.getString("path"), - zipPackage.getFileFromZip(fileItem.getString("filename")) - ) - ); - } - // TODO Get version, and proper mechanism to compare versions - // for each obsolete in obsolete_files - // if obsolete["since"] < this_version ; delete obsolete["path"] - } catch (ZipFileException e) { - LOG.error("Unable to read the zip file.", e); - } catch (FileNotFoundException e) { - LOG.error("The update file was not found.", e); - } catch (IOException e) { - LOG.error("General IO error occurred.", e); - } catch (Exception e) { - LOG.error("Unknown error occurred.", e); - } - try { - this.startNextAdaFsAction(); - } catch (IOException e) { - LOG.error("Error while loading resources: ", e); - } - } - - private void startNextAdaFsAction() throws IOException { - if (adaBleFsQueue.size() == 0) { - return; - } - final AdaBleFsAction nextAction = adaBleFsQueue.getFirst(); - switch (nextAction.method) { - case UPLOAD: - uploadFileStart(); - break; - case DELETE: - deleteFile(); - break; - } - } - - - public void enableNotify(TransactionBuilder builder, boolean enable) { - builder.notify(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), enable); - } - - public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - if (status == BluetoothGatt.GATT_SUCCESS) { - UUID charUuid = characteristic.getUuid(); - if (charUuid.equals(UUID_CHARACTERISTIC_FS_TRANSFER)) { - try { - handleNextStatus(gatt, characteristic); - } catch (IOException e) { - LOG.error("Error handling status: ", e); - } - return true; - } else { - LOG.info("Unexpected onCharacteristicRead: " + GattCharacteristic.toString(characteristic)); - } - } else { - LOG.warn("error reading from characteristic:" + GattCharacteristic.toString(characteristic)); - } - return false; - } - - private void handleNextStatus(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) throws IOException { - // Determine what we are waiting on - final byte[] returned = characteristic.getValue(); - // if doing file upload - boolean handled = false; - switch (returned[0]) { - case RESPONSE_WRITE_FILE: - handled = checkContinueFileUpload(returned); - break; - case RESPONSE_DELETE_FILE: - handled = checkDeleteFile(returned); - break; - case RESPONSE_MAKE_DIRECTORY: - handled = checkMakeDirectory(returned); - break; - case RESPONSE_LIST_DIRECTORY: - // TODO We just throw away the results for now, not sure what to do with them - handled = checkListDirectory(returned); - break; - case RESPONSE_MOVE_FILE_DIRECTORY: - handled = checkMove(returned); - break; - } - // Read bytes - // Check for OK - // queue next event - return; - } - - private void checkStatus(byte status) { - // 0x00 is Error - // 0x01 / STATUS_OK is OK - // 0x05 is error modifying read-only FS - // all others are errors too - if (status != STATUS_OK) { - // Raise an exception? - } - - } - - private boolean checkMove(byte[] returned) { - byte status = returned[1]; - - return true; - } - - private boolean checkListDirectory(byte[] returned) { - /** - * Check that a list directory command worked okay - * - * @return true if everything is okay - */ - // Note that we get one response per entry in the directory. - byte status = returned[1]; - checkStatus(status); - int length = returned[2] + (returned[3] >> 8); - int entryNum = returned[4] + (returned[5] >> 8) + (returned[6] >> 16) + (returned[7] >> 24); - int totalEntries = returned[8] + (returned[9] >> 8) + (returned[10] >> 16) + (returned[11] >> 24); - // returned[12:15] = flags, but only bit 0 matters - int flags = returned[12] + (returned[13] >> 8) + (returned[14] >> 16) + (returned[15] >> 24); - boolean isDirectory = (flags & 1) == 1; - long timeStamp = returned[16] + (returned[17] >> 8) + (returned[18] >> 16) + (returned[19] >> 24) + - (returned[20] >> 32) + (returned[21] >> 40) + (returned[22] >> 48) + (returned[23] >> 56); - int fileSize = returned[24] + (returned[25] >> 8) + (returned[26] >> 16) + (returned[27] >> 24); - // Rest is the path, relative to the path we requested - byte[] stringBytes = new byte[length+1]; - for(int counter = 0; counter < length; counter++) { - stringBytes[counter] = returned[28+counter]; - } -// Map relativeRoot = directoryTree; -// for(String path: listingDirectory) { -// relativeRoot = (Map) relativeRoot.get(path); -// } - try { - String path = new String(stringBytes, "UTF-8"); -// Map here = directoryTree; // TODO Paths returned are relative to where we requested paths from so we need to alter this -// // TODO Remove first / ? -// // Split path on / -// String soFar = ""; -// final String[] paths = path.split("/"); -// for(int counter = 0; counter < paths.length; counter++) { -// final String dir = paths[counter]; -// soFar = soFar + "/" + dir; -// if (here.containsKey(dir)) { -// Object entry = here.get(dir); -// if (counter < (paths.length-1) || isDirectory) { -// // If not at last entry in paths, or if the response says this is a directory -// if (entry instanceof HashMap) { -// // All good, continue; -// } else { -// // our map doesn't list directory, but this is? -// LOG.warn("GadgetBridge cache thinks " + soFar + " is a file, but device thinks it's a directory."); -// here.put(dir, new HashMap()); -// } -// } else { -// // We are at the last string in paths, and response says this isn't a file -// if (entry instanceof String) { -// // All good, continue -// } else { -// LOG.warn("GadgetBridge cache thinks " + soFar + " is a directory, but device thinks it's a file."); -// here.put(dir, dir); -// } -// } -// } else { -// // Our cache doesn't know of soFar -// if (counter < (paths.length-1) || isDirectory) { -// here.put(dir, new HashMap()); -// } else { -// here.put(dir, dir); -// } -// } -// } - } catch (UnsupportedEncodingException e) { - // Pretty sure this branch will never happen - } -// if (entryNum == totalEntries) { -// relativeRoot.put(".", "."); -// } - return entryNum == totalEntries; - } - - private boolean checkMakeDirectory(byte[] returned) { - /** - * Check that a make directory command worked okay - * - * @return true if everything is okay - */ - byte status = returned[1]; - checkStatus(status); - // returned[2:7] are padding - // returned[8:15] are a unix timestamp of the directory - return true; - } - - private boolean checkDeleteFile(byte[] returned) { - /** - * Check that a delete command worked okay - * - * @return true if everything is okay - */ - byte status = returned[1]; - checkStatus(status); - // Not sure what to do here - return true; - } - - private boolean checkContinueFileUpload(byte[] returned) throws IOException { - /** - * Check that a file upload command has completed - * - * @return true if everything is okay - */ - byte status = returned[1]; - checkStatus(status); - // returned[2:3] are padding - locationToWriteTo = returned[4] + (returned[5] << 8) + (returned[6] << 16) + (returned[7] << 24); - // returned[8:15] is uint64 encoding unix timestamp - int sizeLeft = returned[16] + (returned[17] <<8) + (returned[18] << 16) + (returned[19] << 24); - if (sizeLeft > 0) { - uploadNextFileChunk(); - } - return (sizeLeft == 0); - } - - @Override - public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value, int status) { - return onCharacteristicRead(gatt, characteristic, BluetoothGatt.GATT_SUCCESS); - } - - private void uploadNextFileChunk() throws IOException { - final AdaBleFsAction nextAction = adaBleFsQueue.getFirst(); - final int toSendSize = Math.min(chunkSize, nextAction.data.length - bytesOfFileWritten); - ArrayList command = new ArrayList<>(); - command.add(REQUEST_WRITE_FILE_DATA); - command.add(REQUEST_CONTINUED); - command.add(PADDING_BYTE); - command.add(PADDING_BYTE); - command.add((byte) (locationToWriteTo & 0xFF)); // 32-bit offset - command.add((byte) ((locationToWriteTo >> 8) & 0xFF)); - command.add((byte) ((locationToWriteTo >> 16) & 0xFF)); - command.add((byte) ((locationToWriteTo >> 24) & 0xFF)); - command.add((byte) (toSendSize & 0xFF)); // File size as 32-bit - command.add((byte) ((toSendSize >> 8) & 0xFF)); - command.add((byte) ((toSendSize >> 16) & 0xFF)); - command.add((byte) ((toSendSize >> 24) & 0xFF)); - for(int counter = bytesOfFileWritten; counter < toSendSize; counter++) { - command.add(nextAction.data[counter]); - } - byte[] bytes = new byte[command.size()]; - for(int i = 0; i < command.size(); i++) { - bytes[i] = command.get(i); - } - TransactionBuilder builder = performInitialized("Upload file chunk"); - builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes); - builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER)); - builder.queue(); - } - - private void uploadFileInitialise() { - final AdaBleFsAction nextAction = adaBleFsQueue.getFirst(); - // Check if nextAction.filenameorpath directory exists - // get directory - // check directory exists, including parents - // remove file if it already exists - // then uploadFileStart() - } - -// private TriState directoryExists(String[] directoryList) throws IOException { -// Map here = directoryTree; -// String[] soFar; -// String totalPath = "/"; -// for(String directory: directoryList) { -// if (here.containsKey(directory)) { -// Object entry = here.get(directory); -// if (entry instanceof HashMap) { -// here = (Map) entry; -// totalPath += directory + "/"; -// } else { -// // Not a HashMap, must be a string then and so this is a file, not a directory -// return TriState.FALSE; -// } -// } else if (here.containsKey(".")) { -// // We have listed this directory, and the requested path does not exist -// return TriState.FALSE; -// } -// // Request directory listing for here -// totalPath += directory; -// requestListDirectory(totalPath); -// return TriState.UNKNOWN; -// } -// return TriState.TRUE; -// } - - private void uploadFileStart() throws IOException { - bytesOfFileWritten = 0; - // TODO Should this be the time we upload, or should it somehow be the timestamp of the - // resources.zip archive file - long unixTime = System.currentTimeMillis() / 1000L; - final AdaBleFsAction nextAction = adaBleFsQueue.getFirst(); - Vector command = new Vector<>(); - command.add(REQUEST_WRITE_FILE_START); - command.add(PADDING_BYTE); - command.add((byte) (nextAction.filenameorpath.length() & 0xFF)); // 16-bit path length - command.add((byte) ((nextAction.filenameorpath.length() >> 8) & 0xFF)); - command.add((byte) 0x00); // 32-bit offset is 0 for file start - command.add((byte) 0x00); - command.add((byte) 0x00); - command.add((byte) 0x00); - command.add((byte) (unixTime & 0xFF)); // Timestamp as 64-bit - command.add((byte) ((unixTime >> 8) & 0xFF)); - command.add((byte) ((unixTime >> 16) & 0xFF)); - command.add((byte) ((unixTime >> 24) & 0xFF)); - command.add((byte) ((unixTime >> 32) & 0xFF)); - command.add((byte) ((unixTime >> 40) & 0xFF)); - command.add((byte) ((unixTime >> 48) & 0xFF)); - command.add((byte) ((unixTime >> 56) & 0xFF)); - command.add((byte) (nextAction.data.length & 0xFF)); // File size as 32-bit - command.add((byte) ((nextAction.data.length >> 8) & 0xFF)); - command.add((byte) ((nextAction.data.length >> 16) & 0xFF)); - command.add((byte) ((nextAction.data.length >> 24) & 0xFF)); - // Is there a better way to construct a bytes[] ? - byte[] bytes = new byte[command.size()]; - for(int i = 0; i < command.size(); i++) { - bytes[i] = command.get(i); - } - TransactionBuilder builder = performInitialized("Upload file start"); - builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes); - builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER)); - builder.queue(); - return; - } - - private void requestListDirectory(String path) throws IOException { - Vector command = new Vector(); - command.add(REQUEST_LIST_DIRECTORY); - command.add(PADDING_BYTE); - command.add((byte) (path.length() & 0xFF)); // 16-bit path length - command.add((byte) ((path.length() >> 8) & 0xFF)); - for(int count = 0; count < path.length(); count++) { - command.add((byte) path.charAt(count)); - } - // send command, wait for response - // Is there a better way to construct a bytes[] ? - byte[] bytes = new byte[command.size()]; - for(int i = 0; i < command.size(); i++) { - bytes[i] = command.get(i); - } - TransactionBuilder builder = performInitialized("List directory"); - builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes); - builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER)); - builder.queue(); - listingDirectory = Arrays.asList(path.split("/")); - return; - } - private void deleteFile() throws IOException { - final AdaBleFsAction nextAction = adaBleFsQueue.getFirst(); - Vector command = new Vector(); - command.add(REQUEST_DELETE_FILE); - command.add(PADDING_BYTE); - command.add((byte) (nextAction.filenameorpath.length() & 0xFF)); // 16-bit path length - command.add((byte) ((nextAction.filenameorpath.length() >> 8) & 0xFF)); - for(int count = 0; count < nextAction.filenameorpath.length(); count++) { - command.add((byte) nextAction.filenameorpath.charAt(count)); - } - // send command, wait for response - // Is there a better way to construct a bytes[] ? - byte[] bytes = new byte[command.size()]; - for(int i = 0; i < command.size(); i++) { - bytes[i] = command.get(i); - } - TransactionBuilder builder = performInitialized("Delete file or directory"); - builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes); - builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER)); - builder.queue(); - return; - } - - private void makeDirectory() throws IOException { - final AdaBleFsAction nextAction = adaBleFsQueue.getFirst(); - long unixTime = System.currentTimeMillis() / 1000L; - Vector command = new Vector(); - command.add(REQUEST_MAKE_DIRECTORY); - command.add(PADDING_BYTE); - command.add((byte) (nextAction.filenameorpath.length() & 0xFF)); // 16-bit path length - command.add((byte) ((nextAction.filenameorpath.length() >> 8) & 0xFF)); - command.add(PADDING_BYTE); - command.add(PADDING_BYTE); - command.add(PADDING_BYTE); - command.add(PADDING_BYTE); - command.add((byte) (unixTime & 0xFF)); // Timestamp as 64-bit - command.add((byte) ((unixTime >> 8) & 0xFF)); - command.add((byte) ((unixTime >> 16) & 0xFF)); - command.add((byte) ((unixTime >> 24) & 0xFF)); - command.add((byte) ((unixTime >> 32) & 0xFF)); - command.add((byte) ((unixTime >> 40) & 0xFF)); - command.add((byte) ((unixTime >> 48) & 0xFF)); - command.add((byte) ((unixTime >> 56) & 0xFF)); - for(int count = 0; count < nextAction.filenameorpath.length(); count++) { - command.add((byte) nextAction.filenameorpath.charAt(count)); - } - // send command, wait for response - // Is there a better way to construct a bytes[] ? - byte[] bytes = new byte[command.size()]; - for(int i = 0; i < command.size(); i++) { - bytes[i] = command.get(i); - } - TransactionBuilder builder = performInitialized("Create directory"); - builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes); - builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER)); - builder.queue(); - return; - } - - private void moveFileOrDirectory() throws IOException { - final AdaBleFsAction nextAction = adaBleFsQueue.getFirst(); - long unixTime = System.currentTimeMillis() / 1000L; - Vector command = new Vector(); - command.add(REQUEST_MOVE_FILE_DIRECTORY); - command.add(PADDING_BYTE); - command.add((byte) (nextAction.filenameorpath.length() & 0xFF)); // 16-bit path length - command.add((byte) ((nextAction.filenameorpath.length() >> 8) & 0xFF)); - command.add((byte) (nextAction.secondFilenameorpath.length() & 0xFF)); // 16-bit path length - command.add((byte) ((nextAction.secondFilenameorpath.length() >> 8) & 0xFF)); - for(int count = 0; count < nextAction.filenameorpath.length(); count++) { - command.add((byte) nextAction.filenameorpath.charAt(count)); - } - command.add(PADDING_BYTE); - for(int count = 0; count < nextAction.secondFilenameorpath.length(); count++) { - command.add((byte) nextAction.secondFilenameorpath.charAt(count)); - } - // send command, wait for response - // Is there a better way to construct a bytes[] ? - byte[] bytes = new byte[command.size()]; - for(int i = 0; i < command.size(); i++) { - bytes[i] = command.get(i); - } - TransactionBuilder builder = performInitialized("Move file or directory"); - builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes); - builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER)); - builder.queue(); - return; - } - -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsProfile.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsProfile.kt new file mode 100644 index 0000000000..09ad1faddd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsProfile.kt @@ -0,0 +1,339 @@ +/* Copyright (C) 2025 Arjan Schrijver + + 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.btle.profiles.adablefs + +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCharacteristic +import android.content.Context +import android.content.Intent +import android.net.Uri +import nodomain.freeyourgadget.gadgetbridge.R +import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFConstants +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport +import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile +import nodomain.freeyourgadget.gadgetbridge.util.GBZipFile +import nodomain.freeyourgadget.gadgetbridge.util.RemoteFileSystemCache +import nodomain.freeyourgadget.gadgetbridge.util.UriHelper +import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException +import org.json.JSONObject +import org.slf4j.LoggerFactory +import java.io.FileNotFoundException +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.ArrayDeque +import java.util.UUID +import kotlin.math.roundToInt + +class AdaBleFsProfile(support: T) : AbstractBleProfile(support) { + + private val PADDING_BYTE: Byte = 0x00 + private val REQUEST_CONTINUED: Byte = 0x01 + private val REQUEST_WRITE_FILE_START: Byte = 0x20 + private val RESPONSE_WRITE_FILE: Byte = 0x21 + private val REQUEST_WRITE_FILE_DATA: Byte = 0x22 + private val REQUEST_DELETE_FILE: Byte = 0x30 + private val RESPONSE_DELETE_FILE: Byte = 0x31 + private val REQUEST_MAKE_DIRECTORY: Byte = 0x40 + private val RESPONSE_MAKE_DIRECTORY: Byte = 0x41 + private val REQUEST_LIST_DIRECTORY: Byte = 0x50 + private val RESPONSE_LIST_DIRECTORY: Byte = 0x51 + private val REQUEST_MOVE_FILE_DIRECTORY: Byte = 0x60 + private val RESPONSE_MOVE_FILE_DIRECTORY: Byte = 0x61 + private val STATUS_OK: Byte = 0x01 + + companion object { + @JvmField + val UUID_SERVICE_FS: UUID = UUID.fromString("0000febb-0000-1000-8000-00805f9b34fb") + val UUID_CHARACTERISTIC_FS_VERSION: UUID = + UUID.fromString("adaf0100-4669-6c65-5472-616e73666572") + val UUID_CHARACTERISTIC_FS_TRANSFER: UUID = + UUID.fromString("adaf0200-4669-6c65-5472-616e73666572") + + private val LOG = LoggerFactory.getLogger(AdaBleFsProfile::class.java) + } + + private var btleQueue: BtLEQueue? = null + private var currentBuilder: TransactionBuilder? = null + private var device: GBDevice? = null + private val adaBleFsQueue: ArrayDeque = ArrayDeque() + private var offsetToWriteTo = 0 + private var chunkSize = 240 // Maximum possible within MTU + private val remoteFs = RemoteFileSystemCache() + private var listingDirectory: List = emptyList() + private var allActionsCount = 0 + private var currentActionNr = 0 + + fun loadResources(uri: Uri, context: Context, queue: BtLEQueue) { + btleQueue = queue + try { + val uriHelper = UriHelper.get(uri, context) + val zipPackage = GBZipFile(uriHelper.openInputStream()) + val resourcesManifest = JSONObject(String(zipPackage.getFileFromZip("resources.json"))) + val resources = resourcesManifest.getJSONArray("resources") + // TODO: add support for deleting files in the obsolete_files array + for (i in 0 until resources.length()) { + val fileItem = resources.getJSONObject(i) + val filePath = fileItem.getString("path") + val fileName = fileItem.getString("filename") + LOG.info("Adding to queue: UPLOAD, $fileName, $filePath") + adaBleFsQueue.add( + AdaBleFsAction( + AdaBleFsAction.Method.UPLOAD, + filePath, + zipPackage.getFileFromZip(fileName) + ) + ) + allActionsCount += 1 + } + } catch (e: ZipFileException) { + LOG.error("Unable to read the zip file.", e) + } catch (e: FileNotFoundException) { + LOG.error("The update file was not found.", e) + } catch (e: IOException) { + LOG.error("General IO error occurred.", e) + } catch (e: Exception) { + LOG.error("Unknown error occurred.", e) + } + + try { + startNextAdaFsAction() + } catch (e: IOException) { + LOG.error("Error while loading resources: ", e) + } + } + + @Throws(IOException::class) + private fun startNextAdaFsAction() { + val nextAction = adaBleFsQueue.firstOrNull()// ?: return + if (nextAction == null) { + notify(createIntent("", false)) + return + } + currentActionNr++ + when (nextAction.method) { + AdaBleFsAction.Method.UPLOAD -> uploadFileStart() + AdaBleFsAction.Method.DELETE -> deleteFile() + else -> { + LOG.warn("Skipping unimplemented AdaBleFsAction method ${nextAction.method}") + } + } + } + + override fun enableNotify(builder: TransactionBuilder, enable: Boolean) { + builder.notify(UUID_CHARACTERISTIC_FS_TRANSFER, enable) + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt?, + characteristic: BluetoothGattCharacteristic?, + value: ByteArray? + ): Boolean { + if (characteristic?.uuid == UUID_CHARACTERISTIC_FS_TRANSFER) { + try { + handleNextStatus(gatt, characteristic) + } catch (e: IOException) { + LOG.error("Error handling status: ", e) + } + return true + } + return false + } + + @Throws(IOException::class) + private fun handleNextStatus(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic) { + val returned = characteristic.value + + if (returned == null || returned.isEmpty()) { + LOG.warn("Received empty BLE characteristic value for ${characteristic.uuid}") + return + } + + var actionResult = false + when (returned[0]) { + RESPONSE_WRITE_FILE -> actionResult = checkContinueFileUpload(returned) + RESPONSE_DELETE_FILE -> actionResult = checkDeleteFile(returned) + RESPONSE_MAKE_DIRECTORY -> actionResult = checkMakeDirectory(returned) + RESPONSE_LIST_DIRECTORY -> actionResult = checkListDirectory(returned) + RESPONSE_MOVE_FILE_DIRECTORY -> actionResult = checkMove(returned) + else -> LOG.warn("Unknown BLE FS response: ${returned[0]}") + } + if (actionResult) { + adaBleFsQueue.removeFirst() + startNextAdaFsAction() + } + } + + private fun checkStatus(status: Byte) { + if (status != STATUS_OK) throw IOException("Operation failed with status $status") + } + + private fun checkMove(returned: ByteArray) = true.also { checkStatus(returned[1]) } + + private fun checkListDirectory(returned: ByteArray): Boolean { + checkStatus(returned[1]) + + val pathLength = returned[2].toInt() or (returned[3].toInt() shl 8) + val entryNum = returned.sliceArray(4..7).foldIndexed(0) { i, acc, b -> acc or ((b.toInt() and 0xFF) shl (8 * i)) } + val totalEntries = returned.sliceArray(8..11).foldIndexed(0) { i, acc, b -> acc or ((b.toInt() and 0xFF) shl (8 * i)) } + val flags = returned.sliceArray(12..15).foldIndexed(0) { i, acc, b -> acc or ((b.toInt() and 0xFF) shl (8 * i)) } + val isDirectory = (flags and 1) == 1 + val timestamp = returned.sliceArray(16..23).foldIndexed(0L) { i, acc, b -> acc or ((b.toLong() and 0xFF) shl (8 * i)) } + val fileSize = returned.sliceArray(24..27).foldIndexed(0) { i, acc, b -> acc or ((b.toInt() and 0xFF) shl (8 * i)) } + val path = String(returned.copyOfRange(28, 28 + pathLength), Charsets.UTF_8) + + remoteFs.updateEntry(path, AdaFsEntry(path, isDirectory, fileSize, timestamp, flags)) + return entryNum == totalEntries + } + + private fun checkMakeDirectory(returned: ByteArray) = true.also { checkStatus(returned[1]) } + + private fun checkDeleteFile(returned: ByteArray) = true.also { checkStatus(returned[1]) } + + @Throws(IOException::class) + private fun checkContinueFileUpload(returned: ByteArray): Boolean { + checkStatus(returned[1]) + offsetToWriteTo = ByteBuffer.wrap(returned, 4, 4).order(ByteOrder.LITTLE_ENDIAN).int + val sizeLeft = ByteBuffer.wrap(returned, 16, 4).order(ByteOrder.LITTLE_ENDIAN).int + LOG.debug("Status from watch: ${returned[1]}, offset=$offsetToWriteTo, sizeLeft=$sizeLeft") + if (sizeLeft > 0) uploadNextFileChunk() + return sizeLeft == 0 + } + + @Throws(IOException::class) + private fun uploadFileStart() { + val nextAction = adaBleFsQueue.first() + val unixTime = System.currentTimeMillis() / 1000L + val pathBytes = nextAction.filenameorpath.toByteArray() + val buffer = ByteBuffer.allocate(1 + 1 + 2 + 4 + 8 + 4 + pathBytes.size).order(ByteOrder.LITTLE_ENDIAN) + buffer.put(REQUEST_WRITE_FILE_START) + buffer.put(PADDING_BYTE) + buffer.putShort(pathBytes.size.toShort()) + buffer.putInt(0) + buffer.putLong(unixTime) + buffer.putInt(nextAction.data.size) + buffer.put(pathBytes) + + LOG.info("Sending start packet for ${nextAction.filenameorpath} (${nextAction.data.size} bytes)") + currentBuilder = performInitialized("Upload file start") + currentBuilder?.write(UUID_CHARACTERISTIC_FS_TRANSFER, *buffer.array()) + currentBuilder?.queue() + } + + @Throws(IOException::class) + private fun uploadNextFileChunk() { + val nextAction = adaBleFsQueue.first() + val toSendSize = minOf(chunkSize, nextAction.data.size - offsetToWriteTo) + val buffer = ByteBuffer.allocate(2 + 2 + 4 + 4 + toSendSize).order(ByteOrder.LITTLE_ENDIAN) + buffer.put(REQUEST_WRITE_FILE_DATA) + buffer.put(REQUEST_CONTINUED) + buffer.put(PADDING_BYTE) + buffer.put(PADDING_BYTE) + buffer.putInt(offsetToWriteTo + toSendSize) // next chunk offset + buffer.putInt(toSendSize) + buffer.put(nextAction.data, offsetToWriteTo, toSendSize) + + val percentage = (((offsetToWriteTo+toSendSize).toFloat() / nextAction.data.size.toFloat()) * 100).roundToInt() + LOG.info("Sending chunk of $toSendSize bytes ($percentage%) for ${nextAction.filenameorpath}, next chunk starts at offset $offsetToWriteTo") + notify(createIntent(nextAction.filenameorpath, true)) + currentBuilder = performInitialized("Upload file chunk") + currentBuilder?.write(UUID_CHARACTERISTIC_FS_TRANSFER, *buffer.array()) + currentBuilder?.setProgress(R.string.uploading_resources, true, percentage) + currentBuilder?.queue() + } + + @Throws(IOException::class) + private fun deleteFile() { + val nextAction = adaBleFsQueue.first() + val pathBytes = nextAction.filenameorpath.toByteArray() + val buffer = ByteBuffer.allocate(2 + 2 + pathBytes.size).order(ByteOrder.LITTLE_ENDIAN) + buffer.put(REQUEST_DELETE_FILE) + buffer.put(PADDING_BYTE) + buffer.putShort(pathBytes.size.toShort()) + buffer.put(pathBytes) + + currentBuilder = performInitialized("Delete file or directory") + currentBuilder?.write(UUID_CHARACTERISTIC_FS_TRANSFER, *buffer.array()) + currentBuilder?.queue() + } + + @Throws(IOException::class) + private fun makeDirectory() { + val nextAction = adaBleFsQueue.first() + val unixTime = System.currentTimeMillis() / 1000L + val pathBytes = nextAction.filenameorpath.toByteArray() + val buffer = ByteBuffer.allocate(2 + 2 + 4 + 8 + pathBytes.size).order(ByteOrder.LITTLE_ENDIAN) + buffer.put(REQUEST_MAKE_DIRECTORY) + buffer.put(PADDING_BYTE) + buffer.putShort(pathBytes.size.toShort()) + buffer.putInt(0) + buffer.putLong(unixTime) + buffer.put(pathBytes) + + currentBuilder = performInitialized("Create directory") + currentBuilder?.write(UUID_CHARACTERISTIC_FS_TRANSFER, *buffer.array()) + currentBuilder?.queue() + } + + @Throws(IOException::class) + private fun moveFileOrDirectory() { + val nextAction = adaBleFsQueue.first() + val firstPathBytes = nextAction.filenameorpath.toByteArray() + val secondPathBytes = nextAction.secondFilenameorpath.toByteArray() + val buffer = ByteBuffer.allocate(2 + 4 + firstPathBytes.size + secondPathBytes.size).order(ByteOrder.LITTLE_ENDIAN) + buffer.put(REQUEST_MOVE_FILE_DIRECTORY) + buffer.put(PADDING_BYTE) + buffer.putShort(firstPathBytes.size.toShort()) + buffer.putShort(secondPathBytes.size.toShort()) + buffer.put(firstPathBytes) + buffer.put(PADDING_BYTE) + buffer.put(secondPathBytes) + + currentBuilder = performInitialized("Move file or directory") + currentBuilder?.write(UUID_CHARACTERISTIC_FS_TRANSFER, *buffer.array()) + currentBuilder?.queue() + } + + @Throws(IOException::class) + private fun requestListDirectory(path: String) { + val pathBytes = path.toByteArray() + val buffer = ByteBuffer.allocate(2 + 2 + pathBytes.size).order(ByteOrder.LITTLE_ENDIAN) + buffer.put(REQUEST_LIST_DIRECTORY) + buffer.put(PADDING_BYTE) + buffer.putShort(pathBytes.size.toShort()) + buffer.put(pathBytes) + + currentBuilder = performInitialized("List directory") + currentBuilder?.write(UUID_CHARACTERISTIC_FS_TRANSFER, *buffer.array()) + currentBuilder?.queue() + + listingDirectory = path.split("/") + } + + private fun createIntent(filename: String, ongoing: Boolean): Intent { + val intent = Intent(PineTimeJFConstants.ACTION_UPLOAD_PROGRESS) + intent.putExtra("ongoing", ongoing) + intent.putExtra("filename", filename) + intent.putExtra("currentActionNr", currentActionNr) + intent.putExtra("allActionsCount", allActionsCount) + return intent + } + +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaFsEntry.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaFsEntry.kt new file mode 100644 index 0000000000..a839ddcc61 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaFsEntry.kt @@ -0,0 +1,25 @@ +/* Copyright (C) 2025 Arjan Schrijver + + 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.btle.profiles.adablefs + +data class AdaFsEntry( + val path: String, + val isDirectory: Boolean, + val fileSize: Int, + val timestamp: Long, + val flags: Int +) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/RemoteFileSystemCache.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/RemoteFileSystemCache.kt new file mode 100644 index 0000000000..a648808eb2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/RemoteFileSystemCache.kt @@ -0,0 +1,67 @@ +package nodomain.freeyourgadget.gadgetbridge.util + +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.adablefs.AdaFsEntry + +/** + * Type-safe cache for remote filesystem. + * Directories are maps, files are AdaFsEntry objects. + */ +class RemoteFileSystemCache { + + private val root = mutableMapOf() + + fun updateEntry(path: String, entry: AdaFsEntry) { + val parts = path.trim('/').split("/") + var current: MutableMap = root + for ((index, part) in parts.withIndex()) { + if (index == parts.lastIndex) { + current[part] = entry + } else { + current = (current[part] as? MutableMap) ?: mutableMapOf().also { + current[part] = it + } + } + } + } + + fun directoryExists(path: String): Boolean { + val parts = path.trim('/').split("/") + var current: Map = root + for (part in parts) { + val next = current[part] ?: return false + current = next as? Map ?: return false + } + return true + } + + fun getEntry(path: String): AdaFsEntry? { + val parts = path.trim('/').split("/") + var current: Any = root + for (part in parts) { + current = (current as? Map<*, *>)?.get(part) ?: return null + } + return current as? AdaFsEntry + } + + fun removeEntry(path: String) { + val parts = path.trim('/').split("/") + var current: MutableMap = root + for ((index, part) in parts.withIndex()) { + if (index == parts.lastIndex) { + current.remove(part) + } else { + current = (current[part] as? MutableMap) ?: return + } + } + } + + fun listDirectory(path: String): List { + val parts = path.trim('/').split("/") + var current: Any = root + for (part in parts) { + current = (current as? Map)?.get(part) ?: return emptyList() + } + val dir = current as? Map ?: return emptyList() + return dir.values.mapNotNull { it as? AdaFsEntry } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java index 44ba3aae52..5b1c789245 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java @@ -254,6 +254,7 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement percent, speed, averageSpeed, segment, totalSegments)); } }; + private final ResourceUploadProgressListener resourceUploadProgressListener = new ResourceUploadProgressListener() { private final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getContext()); @@ -363,15 +364,31 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement addSupportedService(PineTimeJFConstants.UUID_CHARACTERISTIC_ALERT_NOTIFICATION_EVENT); addSupportedService(PineTimeJFConstants.UUID_SERVICE_MOTION); addSupportedService(PineTimeJFConstants.UUID_SERVICE_HEART_RATE); + addSupportedService(AdaBleFsProfile.UUID_SERVICE_FS); IntentListener mListener = new IntentListener() { @Override public void notify(Intent intent) { String action = intent.getAction(); + LOG.info("IntentListener received an intent with ACTION={}", action); if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) { - handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); + handleDeviceInfo(intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); } else if (BatteryInfoProfile.ACTION_BATTERY_INFO.equals(action)) { - handleBatteryInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO)); + handleBatteryInfo(intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO)); + } else if (PineTimeJFConstants.ACTION_UPLOAD_PROGRESS.equals(action)) { + LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getContext()); + boolean ongoing = intent.getBooleanExtra("ongoing", true); + String progressText = ""; + if (ongoing) { + String filename = intent.getStringExtra("filename"); + int currentActionNr = intent.getIntExtra("currentActionNr", 0); + int allActionsCount = intent.getIntExtra("allActionsCount", 0); + progressText = String.format("Performing action %d of %d\nCurrent filename: %s", currentActionNr, allActionsCount, filename); + } else { + progressText = getContext().getString(R.string.devicestatus_upload_completed); + gbDevice.unsetBusyTask(); + } + manager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT).putExtra(GB.DISPLAY_MESSAGE_MESSAGE, progressText)); } } }; @@ -388,8 +405,8 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement addSupportedProfile(batteryInfoProfile); adaBleFsProfile = new AdaBleFsProfile<>(this); + adaBleFsProfile.addListener(mListener); addSupportedProfile(adaBleFsProfile); - addSupportedService(AdaBleFsProfile.UUID_CHARACTERISTIC_FS_TRANSFER); } private void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) { @@ -647,6 +664,7 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement setInitialized(builder); batteryInfoProfile.requestBatteryInfo(builder); batteryInfoProfile.enableNotify(builder, true); + adaBleFsProfile.enableNotify(builder, true); builder.requestMtu(256); return builder; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/RemoteFileSystemCache.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/RemoteFileSystemCache.java deleted file mode 100644 index fabe164849..0000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/RemoteFileSystemCache.java +++ /dev/null @@ -1,104 +0,0 @@ -/* Copyright (C) 2016-2021 Andreas Shimokawa, Carsten Pfeiffer, João - Paulo Barraca, JohnnySun - - 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.util; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -public class RemoteFileSystemCache { - - private Directory root; - - public RemoteFileSystemCache() { - root = new Directory("/"); - } - - public void addFile(String full_path) { - root.addFile(full_path.substring(1)); // skip first slash - } - - public void addDirectory(String full_path) { - root.addDirectory(full_path.substring(1)); // skip first slash - } - public boolean hasFile(String full_path) { - return root.hasFile(full_path); - } - private class Directory { - String name; - List files; - List directories; - - Directory(String name) { - files = new ArrayList<>(); - directories = new ArrayList<>(); - this.name = name; - } - - void addFile(String filename) { - int indexOfSlash = filename.indexOf('/'); - if (indexOfSlash > 0) { - Directory nextDirectory = getDirectory(filename.substring(0, indexOfSlash)).get(); - nextDirectory.addFile(filename.substring(indexOfSlash + 1)); - } else { - files.add(filename); - } - } - - void addDirectory(String name) { - int indexOfSlash = name.indexOf('/'); - if (indexOfSlash > 0) { - Directory nextDirectory = getDirectory(name.substring(0, indexOfSlash)).get(); - nextDirectory.addFile(name.substring(indexOfSlash + 1)); - } else { - directories.add(new Directory(name)); - } - } - - boolean hasFile(String full_path) { - // Remove leading slash, it may be left if we are root - String path; - if (full_path.charAt(0) == '/') { - path = full_path.substring(1); - } else { - path = full_path; - } - int indexOfSlash = path.indexOf('/'); - if (indexOfSlash >= 0) { - Optional nextDirectory = getDirectory(path.substring(0, indexOfSlash)); - if (nextDirectory.isPresent()) { - String remains = path.substring(indexOfSlash + 1); // Skip past slash - return nextDirectory.get().hasFile(remains); - } - return false; - } else { - return files.contains(path); - } - } - - private Optional getDirectory(String name) { - for(Directory directory: directories) { - if (directory.name == name) { - return Optional.of(directory); - } - } - return null; - } - } -}