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;
- }
- }
-}