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 index 67ea0a04a..1060d88a8 100644 --- 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 @@ -31,8 +31,11 @@ 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; @@ -42,6 +45,7 @@ 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.RemoteFileSystemCache; import nodomain.freeyourgadget.gadgetbridge.util.UriHelper; import nodomain.freeyourgadget.gadgetbridge.util.ZipFile; import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException; @@ -64,6 +68,9 @@ public class AdaBleFsProfile extends Abstra 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"); @@ -76,12 +83,17 @@ public class AdaBleFsProfile extends Abstra 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) { @@ -221,12 +233,55 @@ public class AdaBleFsProfile extends Abstra for(int counter = 0; counter < length; counter++) { stringBytes[counter] = returned[28+counter]; } - // Just throwing away this path? + 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; } @@ -307,6 +362,41 @@ public class AdaBleFsProfile extends Abstra builder.queue(getQueue()); } + 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) { + 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() { bytesOfFileWritten = 0; // TODO Should this be the time we upload, or should it somehow be the timestamp of the @@ -346,15 +436,14 @@ public class AdaBleFsProfile extends Abstra return; } - private void requestListDirectory() { - final AdaBleFsAction nextAction = adaBleFsQueue.getFirst(); + private void requestListDirectory(String path) { Vector command = new Vector(); command.add(REQUEST_LIST_DIRECTORY); 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)); + 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[] ? @@ -366,6 +455,7 @@ public class AdaBleFsProfile extends Abstra builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes); builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER)); builder.queue(getQueue()); + listingDirectory = Arrays.asList(path.split("/")); return; } private void deleteFile() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/RemoteFileSystemCache.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/RemoteFileSystemCache.java new file mode 100644 index 000000000..b17318b74 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/RemoteFileSystemCache.java @@ -0,0 +1,103 @@ +/* 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; + +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; + } + } +}