diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeInstallHandler.java
index 63475275b5..e1a4e99e2f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeInstallHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeInstallHandler.java
@@ -17,6 +17,8 @@
package nodomain.freeyourgadget.gadgetbridge.devices.pinetime;
import android.app.Activity;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import android.content.Context;
import android.net.Uri;
@@ -49,27 +51,41 @@ public class PineTimeInstallHandler implements InstallHandler {
private final Context context;
+ public enum InfiniTimeUpdateType {
+ UNKNOWN,
+ DFU,
+ RESOURCES
+ }
+ public InfiniTimeUpdateType updateType;
+
private InfiniTimeDFUPackage dfuPackageManifest;
public PineTimeInstallHandler(Uri uri, Context context) {
this.context = context;
+ updateType = InfiniTimeUpdateType.UNKNOWN;
UriHelper uriHelper;
try {
uriHelper = UriHelper.get(uri, this.context);
- GBZipFile dfuPackage = new GBZipFile(uriHelper.openInputStream());
- String manifest = new String(dfuPackage.getFileFromZip("manifest.json"));
+ GBZipFile zipPackage = new GBZipFile(uriHelper.openInputStream());
+ if (zipPackage.fileExists("manifest.json")) {
+ updateType = InfiniTimeUpdateType.DFU;
+ String manifest = new String(zipPackage.getFileFromZip("manifest.json"));
- if (!manifest.trim().isEmpty()) {
- dfuPackageManifest = new Gson().fromJson(manifest.trim(), InfiniTimeDFUPackage.class);
+ if (!manifest.trim().isEmpty()) {
+ dfuPackageManifest = new Gson().fromJson(manifest.trim(), InfiniTimeDFUPackage.class);
+ }
+ } else if (zipPackage.fileExists("resources.json")) {
+ updateType = InfiniTimeUpdateType.RESOURCES;
+ } else {
+ LOG.error("Unable to determine update type, no manifest.json or resources.json file found.");
}
-
} catch (ZipFileException e) {
- LOG.error("Unable to read manifest file.", e);
+ LOG.error("Unable to read the zip file.", e);
} catch (FileNotFoundException e) {
- LOG.error("The DFU file was not found.", e);
+ LOG.error("The update file was not found.", e);
} catch (IOException e) {
LOG.error("General IO error occurred.", e);
} catch (Exception e) {
@@ -104,7 +120,11 @@ public class PineTimeInstallHandler implements InstallHandler {
GenericItem installItem = new GenericItem();
installItem.setIcon(R.drawable.ic_firmware);
- installItem.setName("PineTime firmware");
+ if (updateType == InfiniTimeUpdateType.DFU) {
+ installItem.setName("PineTime firmware");
+ } else {
+ installItem.setName("PineTime resources");
+ }
installItem.setDetails(getVersion());
installActivity.setInfoText(context.getString(R.string.firmware_install_warning, "(unknown)"));
@@ -124,19 +144,28 @@ public class PineTimeInstallHandler implements InstallHandler {
@Override
public boolean isValid() {
- return dfuPackageManifest != null &&
- dfuPackageManifest.manifest != null &&
- dfuPackageManifest.manifest.application != null &&
- dfuPackageManifest.manifest.application.bin_file != null;
+ if (updateType == InfiniTimeUpdateType.DFU) {
+ return dfuPackageManifest != null &&
+ dfuPackageManifest.manifest != null &&
+ dfuPackageManifest.manifest.application != null &&
+ dfuPackageManifest.manifest.application.bin_file != null;
+ } else if (updateType == InfiniTimeUpdateType.RESOURCES) {
+ return true; // TODO What counts as valid for a resource update?
+ } else { // updateType == UNKNOWN
+ return false;
+ }
}
// TODO: obtain version information from manifest file instead
private String getVersion() {
- String binFileName = dfuPackageManifest.manifest.application.bin_file;
- Matcher regexMatcher = binNameVersionPattern.matcher(binFileName);
+ if (updateType == InfiniTimeUpdateType.DFU) {
+ String binFileName = dfuPackageManifest.manifest.application.bin_file;
+ Matcher regexMatcher = binNameVersionPattern.matcher(binFileName);
- if (regexMatcher.matches())
- return regexMatcher.group(1);
+ if (regexMatcher.matches())
+ return regexMatcher.group(1);
+ }
+ // TODO Get version of a resources.package
return "(Unknown version)";
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/ResourceUploadProgressListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/ResourceUploadProgressListener.java
new file mode 100644
index 0000000000..8e4cf30131
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/ResourceUploadProgressListener.java
@@ -0,0 +1,35 @@
+/* Copyright (C) 2016-2022 Andreas Shimokawa, Carsten Pfeiffer, JF, Sebastian
+ Kranz, Taavi Eomäe
+
+ 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;
+
+public class ResourceUploadProgressListener {
+ public void onDeviceConnecting(final String mac) { }
+ public void onDeviceConnected(final String mac) { }
+ public void onUploadStarting(final String mac) { }
+ public void onDeviceDisconnecting(final String mac) { }
+ public void onDeviceDisconnected(final String mac) { }
+ public void onUploadCompleted(final String mac) { }
+ public void onUploadAborted(final String mac) { }
+ public void onError(final String mac, int error, int errorType, final String message) { }
+ public void onProgressChanged(final String mac,
+ int percent,
+ float speed,
+ float averageSpeed,
+ int segment,
+ int totalSegments) { }
+}
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
new file mode 100644
index 0000000000..7972ca6640
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsAction.java
@@ -0,0 +1,18 @@
+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/AdaBleFsProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsProfile.java
new file mode 100644
index 0000000000..1060d88a86
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/adablefs/AdaBleFsProfile.java
@@ -0,0 +1,550 @@
+/* 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.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;
+
+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);
+ ZipFile zipPackage = new ZipFile(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);
+ }
+ this.startNextAdaFsAction();
+ }
+
+ private void startNextAdaFsAction() {
+ 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)) {
+ handleNextStatus(gatt, characteristic);
+ 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) {
+ // 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) {
+ /**
+ * 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 onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+ return onCharacteristicRead(gatt, characteristic, BluetoothGatt.GATT_SUCCESS);
+ }
+
+ private void uploadNextFileChunk() {
+ 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 = new TransactionBuilder("Upload file chunk");
+ builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes);
+ builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER));
+ 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
+ // 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 = new TransactionBuilder("Upload file start");
+ builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes);
+ builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER));
+ builder.queue(getQueue());
+ return;
+ }
+
+ private void requestListDirectory(String path) {
+ 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 = new TransactionBuilder("List directory");
+ 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() {
+ 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 = new TransactionBuilder("Delete file or directory");
+ builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes);
+ builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER));
+ builder.queue(getQueue());
+ return;
+ }
+
+ private void makeDirectory() {
+ 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 = new TransactionBuilder("Create directory");
+ builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes);
+ builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER));
+ builder.queue(getQueue());
+ return;
+ }
+
+ private void moveFileOrDirectory() {
+ 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 = new TransactionBuilder("Move file or directory");
+ builder.write(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER), bytes);
+ builder.read(getCharacteristic(UUID_CHARACTERISTIC_FS_TRANSFER));
+ builder.queue(getQueue());
+ return;
+ }
+
+}
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 e4171885f1..d85b21bff6 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
@@ -62,7 +62,6 @@ import no.nordicsemi.android.dfu.DfuServiceInitiator;
import no.nordicsemi.android.dfu.DfuServiceListenerHelper;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
-import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
@@ -92,6 +91,10 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.adablefs.AdaBleFsProfile;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.ResourceUploadProgressListener;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
@@ -113,6 +116,8 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
private final DeviceInfoProfile deviceInfoProfile;
private final BatteryInfoProfile batteryInfoProfile;
+ private final AdaBleFsProfile adaBleFsProfile;
+
private final int MaxNotificationLength = 100;
private final int CutNotificationTitleMinAt = 25;
private int firmwareVersionMajor = 0;
@@ -250,6 +255,101 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
percent, speed, averageSpeed, segment, totalSegments));
}
};
+ private final ResourceUploadProgressListener resourceUploadProgressListener = new ResourceUploadProgressListener() {
+ private final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getContext());
+
+ /**
+ * Sets the progress bar to indeterminate or not, also makes it visible
+ *
+ * @param indeterminate if indeterminate
+ */
+ public void setIndeterminate(boolean indeterminate) {
+ manager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_BAR).putExtra(GB.PROGRESS_BAR_INDETERMINATE, indeterminate));
+ }
+
+ /**
+ * Sets the status text and logs it
+ */
+ public void setProgress(int progress) {
+ manager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_BAR).putExtra(GB.PROGRESS_BAR_PROGRESS, progress));
+ }
+
+ /**
+ * Sets the text that describes progress
+ *
+ * @param progressText text to display
+ */
+ public void setProgressText(String progressText) {
+ manager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT).putExtra(GB.DISPLAY_MESSAGE_MESSAGE, progressText));
+ }
+
+ @Override
+ public void onDeviceConnecting(final String mac) {
+ this.setIndeterminate(true);
+ this.setProgressText(getContext().getString(R.string.devicestatus_connecting));
+ }
+
+ @Override
+ public void onDeviceConnected(final String mac) {
+ this.setIndeterminate(true);
+ this.setProgressText(getContext().getString(R.string.devicestatus_connected));
+ }
+
+ @Override
+ public void onUploadStarting(final String mac) {
+ this.setIndeterminate(true);
+ this.setProgressText(getContext().getString(R.string.devicestatus_upload_starting));
+ }
+
+ @Override
+ public void onDeviceDisconnecting(final String mac) {
+ this.setProgressText(getContext().getString(R.string.devicestatus_disconnecting));
+ }
+
+ @Override
+ public void onDeviceDisconnected(final String mac) {
+ this.setIndeterminate(true);
+ this.setProgressText(getContext().getString(R.string.devicestatus_disconnected));
+ }
+
+ @Override
+ public void onUploadCompleted(final String mac) {
+ this.setProgressText(getContext().getString(R.string.devicestatus_upload_completed));
+ this.setIndeterminate(false);
+ this.setProgress(100);
+
+ handler = null;
+ controller = null;
+ gbDevice.unsetBusyTask();
+ // TODO: Request reconnection
+ }
+
+ @Override
+ public void onUploadAborted(final String mac) {
+ this.setProgressText(getContext().getString(R.string.devicestatus_upload_aborted));
+ gbDevice.unsetBusyTask();
+ }
+
+ @Override
+ public void onError(final String mac, int error, int errorType, final String message) {
+ this.setProgressText(getContext().getString(R.string.devicestatus_upload_failed));
+ gbDevice.unsetBusyTask();
+ }
+
+ @Override
+ public void onProgressChanged(final String mac,
+ int percent,
+ float speed,
+ float averageSpeed,
+ int segment,
+ int totalSegments) {
+ this.setProgress(percent);
+ this.setIndeterminate(false);
+ this.setProgressText(String.format(Locale.ENGLISH,
+ getContext().getString(R.string.firmware_update_progress),
+ percent, speed, averageSpeed, segment, totalSegments));
+ }
+ };
public PineTimeJFSupport() {
super(LOG);
@@ -287,6 +387,10 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
batteryInfoProfile = new BatteryInfoProfile<>(this);
batteryInfoProfile.addListener(mListener);
addSupportedProfile(batteryInfoProfile);
+
+ adaBleFsProfile = new AdaBleFsProfile<>(this);
+ addSupportedProfile(adaBleFsProfile);
+ addSupportedService(AdaBleFsProfile.UUID_CHARACTERISTIC_FS_TRANSFER);
}
private void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) {
@@ -444,9 +548,42 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
@Override
public void onInstallApp(Uri uri, @NonNull final Bundle options) {
- try {
- handler = new PineTimeInstallHandler(uri, getContext());
+ handler = new PineTimeInstallHandler(uri, getContext());
+ if (!handler.isValid()) {
+ LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT)
+ .putExtra(GB.DISPLAY_MESSAGE_MESSAGE, getContext().getString(R.string.fwinstaller_firmware_not_compatible_to_device)));
+ return;
+ }
+ if (handler.updateType == PineTimeInstallHandler.InfiniTimeUpdateType.DFU) {
+ installDfu(uri);
+ } else if (handler.updateType == PineTimeInstallHandler.InfiniTimeUpdateType.RESOURCES) {
+ installResource(uri);
+ }
+ }
+ public void installResource(Uri uri) {
+ try {
+ gbDevice.setBusyTask("resources upgrade");
+ adaBleFsProfile.loadResources(uri, getContext(), getQueue());
+
+ LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_BAR)
+ .putExtra(GB.PROGRESS_BAR_INDETERMINATE, true)
+ );
+ LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT)
+ .putExtra(GB.DISPLAY_MESSAGE_MESSAGE, getContext().getString(R.string.devicestatus_upload_starting))
+ );
+ } catch (Exception ex) {
+ GB.toast(getContext(), getContext().getString(R.string.updatefirmwareoperation_write_failed) + ":" + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
+ if (gbDevice.isBusy() && gbDevice.getBusyTask().equals("resources upgrade")) {
+ gbDevice.unsetBusyTask();
+ }
+ }
+
+ }
+
+ public void installDfu(Uri uri) {
+ handler = new PineTimeInstallHandler(uri, getContext());
+ try {
if (handler.isValid()) {
gbDevice.setBusyTask(R.string.updating_firmware, getContext());
DfuServiceInitiator starter = new DfuServiceInitiator(getDevice().getAddress())
@@ -470,7 +607,7 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
} else {
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT)
.putExtra(GB.DISPLAY_MESSAGE_MESSAGE, getContext().getString(R.string.fwinstaller_firmware_not_compatible_to_device)));
- }
+ }
} catch (Exception ex) {
GB.toast(getContext(), getContext().getString(R.string.updatefirmwareoperation_write_failed) + ":" + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
if (gbDevice.isBusy() && gbDevice.getBusyTask().equals("firmware upgrade")) {
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 0000000000..b17318b745
--- /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;
+ }
+ }
+}