mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
InfiniTime: Refactor and fix resource uploader
This commit is contained in:
committed by
Arjan Schrijver
parent
320dde9a0e
commit
5690d6f846
+1
@@ -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_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 UUID UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb");
|
||||||
|
|
||||||
|
public static final String ACTION_UPLOAD_PROGRESS = "PINETIME_UPLOAD_PROGRESS";
|
||||||
}
|
}
|
||||||
|
|||||||
-18
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+36
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
-559
@@ -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 <http://www.gnu.org/licenses/>. */
|
|
||||||
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<T extends AbstractBTLESingleDeviceSupport> 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<AdaBleFsAction> adaBleFsQueue;
|
|
||||||
int bytesOfFileWritten;
|
|
||||||
int locationToWriteTo;
|
|
||||||
int chunkSize;
|
|
||||||
|
|
||||||
private RemoteFileSystemCache remoteFs;
|
|
||||||
List<String> 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<String, Object> relativeRoot = directoryTree;
|
|
||||||
// for(String path: listingDirectory) {
|
|
||||||
// relativeRoot = (Map<String, Object>) relativeRoot.get(path);
|
|
||||||
// }
|
|
||||||
try {
|
|
||||||
String path = new String(stringBytes, "UTF-8");
|
|
||||||
// Map<String, Object> 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<String, Object>());
|
|
||||||
// }
|
|
||||||
// } 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<String, Object>());
|
|
||||||
// } 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<Byte> 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<String, Object> 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<String, Object>) 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<Byte> 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<Byte> command = new Vector<Byte>();
|
|
||||||
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<Byte> command = new Vector<Byte>();
|
|
||||||
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<Byte> command = new Vector<Byte>();
|
|
||||||
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<Byte> command = new Vector<Byte>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
+339
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||||
|
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<T : AbstractBTLESingleDeviceSupport>(support: T) : AbstractBleProfile<T>(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<AdaBleFsAction> = ArrayDeque()
|
||||||
|
private var offsetToWriteTo = 0
|
||||||
|
private var chunkSize = 240 // Maximum possible within MTU
|
||||||
|
private val remoteFs = RemoteFileSystemCache()
|
||||||
|
private var listingDirectory: List<String> = 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+25
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||||
|
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
|
||||||
|
)
|
||||||
+67
@@ -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<String, Any>()
|
||||||
|
|
||||||
|
fun updateEntry(path: String, entry: AdaFsEntry) {
|
||||||
|
val parts = path.trim('/').split("/")
|
||||||
|
var current: MutableMap<String, Any> = root
|
||||||
|
for ((index, part) in parts.withIndex()) {
|
||||||
|
if (index == parts.lastIndex) {
|
||||||
|
current[part] = entry
|
||||||
|
} else {
|
||||||
|
current = (current[part] as? MutableMap<String, Any>) ?: mutableMapOf<String, Any>().also {
|
||||||
|
current[part] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun directoryExists(path: String): Boolean {
|
||||||
|
val parts = path.trim('/').split("/")
|
||||||
|
var current: Map<String, Any> = root
|
||||||
|
for (part in parts) {
|
||||||
|
val next = current[part] ?: return false
|
||||||
|
current = next as? Map<String, Any> ?: 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<String, Any> = root
|
||||||
|
for ((index, part) in parts.withIndex()) {
|
||||||
|
if (index == parts.lastIndex) {
|
||||||
|
current.remove(part)
|
||||||
|
} else {
|
||||||
|
current = (current[part] as? MutableMap<String, Any>) ?: return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listDirectory(path: String): List<AdaFsEntry> {
|
||||||
|
val parts = path.trim('/').split("/")
|
||||||
|
var current: Any = root
|
||||||
|
for (part in parts) {
|
||||||
|
current = (current as? Map<String, Any>)?.get(part) ?: return emptyList()
|
||||||
|
}
|
||||||
|
val dir = current as? Map<String, Any> ?: return emptyList()
|
||||||
|
return dir.values.mapNotNull { it as? AdaFsEntry }
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-3
@@ -254,6 +254,7 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
|
|||||||
percent, speed, averageSpeed, segment, totalSegments));
|
percent, speed, averageSpeed, segment, totalSegments));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private final ResourceUploadProgressListener resourceUploadProgressListener = new ResourceUploadProgressListener() {
|
private final ResourceUploadProgressListener resourceUploadProgressListener = new ResourceUploadProgressListener() {
|
||||||
private final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getContext());
|
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_CHARACTERISTIC_ALERT_NOTIFICATION_EVENT);
|
||||||
addSupportedService(PineTimeJFConstants.UUID_SERVICE_MOTION);
|
addSupportedService(PineTimeJFConstants.UUID_SERVICE_MOTION);
|
||||||
addSupportedService(PineTimeJFConstants.UUID_SERVICE_HEART_RATE);
|
addSupportedService(PineTimeJFConstants.UUID_SERVICE_HEART_RATE);
|
||||||
|
addSupportedService(AdaBleFsProfile.UUID_SERVICE_FS);
|
||||||
|
|
||||||
IntentListener mListener = new IntentListener() {
|
IntentListener mListener = new IntentListener() {
|
||||||
@Override
|
@Override
|
||||||
public void notify(Intent intent) {
|
public void notify(Intent intent) {
|
||||||
String action = intent.getAction();
|
String action = intent.getAction();
|
||||||
|
LOG.info("IntentListener received an intent with ACTION={}", action);
|
||||||
if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(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)) {
|
} 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);
|
addSupportedProfile(batteryInfoProfile);
|
||||||
|
|
||||||
adaBleFsProfile = new AdaBleFsProfile<>(this);
|
adaBleFsProfile = new AdaBleFsProfile<>(this);
|
||||||
|
adaBleFsProfile.addListener(mListener);
|
||||||
addSupportedProfile(adaBleFsProfile);
|
addSupportedProfile(adaBleFsProfile);
|
||||||
addSupportedService(AdaBleFsProfile.UUID_CHARACTERISTIC_FS_TRANSFER);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) {
|
private void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) {
|
||||||
@@ -647,6 +664,7 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
|
|||||||
setInitialized(builder);
|
setInitialized(builder);
|
||||||
batteryInfoProfile.requestBatteryInfo(builder);
|
batteryInfoProfile.requestBatteryInfo(builder);
|
||||||
batteryInfoProfile.enableNotify(builder, true);
|
batteryInfoProfile.enableNotify(builder, true);
|
||||||
|
adaBleFsProfile.enableNotify(builder, true);
|
||||||
|
|
||||||
builder.requestMtu(256);
|
builder.requestMtu(256);
|
||||||
return builder;
|
return builder;
|
||||||
|
|||||||
-104
@@ -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 <http://www.gnu.org/licenses/>. */
|
|
||||||
|
|
||||||
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<String> files;
|
|
||||||
List<Directory> 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<Directory> 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<Directory> getDirectory(String name) {
|
|
||||||
for(Directory directory: directories) {
|
|
||||||
if (directory.name == name) {
|
|
||||||
return Optional.of(directory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user