mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Resource uploading via BLEFS - wip
This is a commit of partial progress, this shouldn't even compile
This commit is contained in:
committed by
Arjan Schrijver
parent
3118d6c4fe
commit
9c93d11229
+45
-16
@@ -17,6 +17,8 @@
|
|||||||
package nodomain.freeyourgadget.gadgetbridge.devices.pinetime;
|
package nodomain.freeyourgadget.gadgetbridge.devices.pinetime;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
@@ -49,27 +51,41 @@ public class PineTimeInstallHandler implements InstallHandler {
|
|||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
|
public enum InfiniTimeUpdateType {
|
||||||
|
UNKNOWN,
|
||||||
|
DFU,
|
||||||
|
RESOURCES
|
||||||
|
}
|
||||||
|
public InfiniTimeUpdateType updateType;
|
||||||
|
|
||||||
private InfiniTimeDFUPackage dfuPackageManifest;
|
private InfiniTimeDFUPackage dfuPackageManifest;
|
||||||
|
|
||||||
public PineTimeInstallHandler(Uri uri, Context context) {
|
public PineTimeInstallHandler(Uri uri, Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
|
updateType = InfiniTimeUpdateType.UNKNOWN;
|
||||||
UriHelper uriHelper;
|
UriHelper uriHelper;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
uriHelper = UriHelper.get(uri, this.context);
|
uriHelper = UriHelper.get(uri, this.context);
|
||||||
|
|
||||||
GBZipFile dfuPackage = new GBZipFile(uriHelper.openInputStream());
|
GBZipFile zipPackage = new GBZipFile(uriHelper.openInputStream());
|
||||||
String manifest = new String(dfuPackage.getFileFromZip("manifest.json"));
|
if (zipPackage.fileExists("manifest.json")) {
|
||||||
|
updateType = InfiniTimeUpdateType.DFU;
|
||||||
|
String manifest = new String(zipPackage.getFileFromZip("manifest.json"));
|
||||||
|
|
||||||
if (!manifest.trim().isEmpty()) {
|
if (!manifest.trim().isEmpty()) {
|
||||||
dfuPackageManifest = new Gson().fromJson(manifest.trim(), InfiniTimeDFUPackage.class);
|
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) {
|
} catch (ZipFileException e) {
|
||||||
LOG.error("Unable to read manifest file.", e);
|
LOG.error("Unable to read the zip file.", e);
|
||||||
} catch (FileNotFoundException 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) {
|
} catch (IOException e) {
|
||||||
LOG.error("General IO error occurred.", e);
|
LOG.error("General IO error occurred.", e);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -104,7 +120,11 @@ public class PineTimeInstallHandler implements InstallHandler {
|
|||||||
|
|
||||||
GenericItem installItem = new GenericItem();
|
GenericItem installItem = new GenericItem();
|
||||||
installItem.setIcon(R.drawable.ic_firmware);
|
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());
|
installItem.setDetails(getVersion());
|
||||||
|
|
||||||
installActivity.setInfoText(context.getString(R.string.firmware_install_warning, "(unknown)"));
|
installActivity.setInfoText(context.getString(R.string.firmware_install_warning, "(unknown)"));
|
||||||
@@ -124,19 +144,28 @@ public class PineTimeInstallHandler implements InstallHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isValid() {
|
public boolean isValid() {
|
||||||
return dfuPackageManifest != null &&
|
if (updateType == InfiniTimeUpdateType.DFU) {
|
||||||
dfuPackageManifest.manifest != null &&
|
return dfuPackageManifest != null &&
|
||||||
dfuPackageManifest.manifest.application != null &&
|
dfuPackageManifest.manifest != null &&
|
||||||
dfuPackageManifest.manifest.application.bin_file != 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
|
// TODO: obtain version information from manifest file instead
|
||||||
private String getVersion() {
|
private String getVersion() {
|
||||||
String binFileName = dfuPackageManifest.manifest.application.bin_file;
|
if (updateType == InfiniTimeUpdateType.DFU) {
|
||||||
Matcher regexMatcher = binNameVersionPattern.matcher(binFileName);
|
String binFileName = dfuPackageManifest.manifest.application.bin_file;
|
||||||
|
Matcher regexMatcher = binNameVersionPattern.matcher(binFileName);
|
||||||
|
|
||||||
if (regexMatcher.matches())
|
if (regexMatcher.matches())
|
||||||
return regexMatcher.group(1);
|
return regexMatcher.group(1);
|
||||||
|
}
|
||||||
|
// TODO Get version of a resources.package
|
||||||
return "(Unknown version)";
|
return "(Unknown version)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
@@ -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 <http://www.gnu.org/licenses/>. */
|
||||||
|
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) { }
|
||||||
|
}
|
||||||
+18
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+550
@@ -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 <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.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<T extends AbstractBTLEDeviceSupport> 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);
|
||||||
|
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<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) {
|
||||||
|
/**
|
||||||
|
* 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<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 = 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<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() {
|
||||||
|
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 = 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<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 = 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<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 = 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<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 = 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<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 = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+141
-4
@@ -62,7 +62,6 @@ import no.nordicsemi.android.dfu.DfuServiceInitiator;
|
|||||||
import no.nordicsemi.android.dfu.DfuServiceListenerHelper;
|
import no.nordicsemi.android.dfu.DfuServiceListenerHelper;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
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.WorldClock;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather;
|
import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport;
|
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.BLETypeConversions;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||||
@@ -113,6 +116,8 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
|
|||||||
private final DeviceInfoProfile<PineTimeJFSupport> deviceInfoProfile;
|
private final DeviceInfoProfile<PineTimeJFSupport> deviceInfoProfile;
|
||||||
private final BatteryInfoProfile<PineTimeJFSupport> batteryInfoProfile;
|
private final BatteryInfoProfile<PineTimeJFSupport> batteryInfoProfile;
|
||||||
|
|
||||||
|
private final AdaBleFsProfile<PineTimeJFSupport> adaBleFsProfile;
|
||||||
|
|
||||||
private final int MaxNotificationLength = 100;
|
private final int MaxNotificationLength = 100;
|
||||||
private final int CutNotificationTitleMinAt = 25;
|
private final int CutNotificationTitleMinAt = 25;
|
||||||
private int firmwareVersionMajor = 0;
|
private int firmwareVersionMajor = 0;
|
||||||
@@ -250,6 +255,101 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
|
|||||||
percent, speed, averageSpeed, segment, totalSegments));
|
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() {
|
public PineTimeJFSupport() {
|
||||||
super(LOG);
|
super(LOG);
|
||||||
@@ -287,6 +387,10 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
|
|||||||
batteryInfoProfile = new BatteryInfoProfile<>(this);
|
batteryInfoProfile = new BatteryInfoProfile<>(this);
|
||||||
batteryInfoProfile.addListener(mListener);
|
batteryInfoProfile.addListener(mListener);
|
||||||
addSupportedProfile(batteryInfoProfile);
|
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) {
|
private void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) {
|
||||||
@@ -444,9 +548,42 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInstallApp(Uri uri, @NonNull final Bundle options) {
|
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()) {
|
if (handler.isValid()) {
|
||||||
gbDevice.setBusyTask(R.string.updating_firmware, getContext());
|
gbDevice.setBusyTask(R.string.updating_firmware, getContext());
|
||||||
DfuServiceInitiator starter = new DfuServiceInitiator(getDevice().getAddress())
|
DfuServiceInitiator starter = new DfuServiceInitiator(getDevice().getAddress())
|
||||||
@@ -470,7 +607,7 @@ public class PineTimeJFSupport extends AbstractBTLESingleDeviceSupport implement
|
|||||||
} else {
|
} else {
|
||||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT)
|
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)));
|
.putExtra(GB.DISPLAY_MESSAGE_MESSAGE, getContext().getString(R.string.fwinstaller_firmware_not_compatible_to_device)));
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
GB.toast(getContext(), getContext().getString(R.string.updatefirmwareoperation_write_failed) + ":" + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR, 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")) {
|
if (gbDevice.isBusy() && gbDevice.getBusyTask().equals("firmware upgrade")) {
|
||||||
|
|||||||
+103
@@ -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 <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
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<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