diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilFileReader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilFileReader.java
new file mode 100644
index 000000000..374e35b42
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilFileReader.java
@@ -0,0 +1,183 @@
+/* Copyright (C) 2021 Arjan Schrijver, Daniel Dakhno
+
+ 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.devices.qhybrid;
+
+import android.content.Context;
+import android.net.Uri;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+
+import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
+
+/**
+ * Reads and parses files meant to be uploaded to Fossil Hybrid Q & HR watches.
+ * These can be firmware files, watch apps and watchfaces (HR only).
+ */
+public class FossilFileReader {
+ private static final Logger LOG = LoggerFactory.getLogger(FossilFileReader.class);
+ private final UriHelper uriHelper;
+ private boolean isValid = false;
+ private boolean isFirmware = false;
+ private boolean isApp = false;
+ private boolean isWatchface = false;
+ private String foundVersion = "(Unknown version)";
+ private String foundName = "(unknown)";
+
+ public FossilFileReader(Uri uri, Context context) throws IOException {
+ uriHelper = UriHelper.get(uri, context);
+
+ try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
+ // Read just the first 32 bytes for file type detection
+ byte[] bytes = new byte[32];
+ int read = in.read(bytes);
+ in.close();
+ if (read < 32) {
+ isValid = false;
+ return;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(bytes);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+
+ short handle = buf.getShort();
+ short version = buf.getShort();
+ if ((handle == 5630) && (version == 3)) {
+ // This is a watch app or watch face
+ isValid = true;
+ isApp = true;
+ parseApp();
+ return;
+ }
+
+ // Back to byte 0 for firmware detection
+ buf.rewind();
+ int header0 = buf.getInt();
+ buf.getInt(); // size
+ int header2 = buf.getInt();
+ int header3 = buf.getInt();
+ if (header0 != 1 || header2 != 0x00012000 || header3 != 0x00012000) {
+ return;
+ }
+
+ buf.getInt(); // unknown
+ isValid = true;
+ isFirmware = true;
+ parseFirmware();
+ } catch (Exception e) {
+ LOG.warn("Error during Fossil file parsing", e);
+ }
+ }
+
+ private void parseFirmware() throws IOException {
+ InputStream in = new BufferedInputStream(uriHelper.openInputStream());
+ byte[] bytes = new byte[in.available()];
+ in.read(bytes);
+ in.close();
+ ByteBuffer buf = ByteBuffer.wrap(bytes);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.position(20);
+ int version1 = buf.get() % 0xff;
+ int version2 = buf.get() & 0xff;
+ foundVersion = "DN1.0." + version1 + "." + version2;
+ foundName = "Fossil Hybrid HR firmware";
+ }
+
+ private void parseApp() throws IOException {
+ InputStream in = new BufferedInputStream(uriHelper.openInputStream());
+ byte[] bytes = new byte[in.available()];
+ in.read(bytes);
+ in.close();
+ ByteBuffer buf = ByteBuffer.wrap(bytes);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ buf.position(8); // skip file handle and version
+ int fileSize = buf.getInt();
+ foundVersion = (int)buf.get() + "." + (int)buf.get() + "." + (int)buf.get() + "." + (int)buf.get();
+ buf.position(buf.position() + 8); // skip null bytes
+ int jerryStart = buf.getInt();
+ int appIconStart = buf.getInt();
+ int layout_start = buf.getInt();
+ int display_name_start = buf.getInt();
+ int display_name_start_2 = buf.getInt();
+ int config_start = buf.getInt();
+ int file_end = buf.getInt();
+ buf.position(jerryStart);
+
+ ArrayList filenamesCode = parseAppFilenames(buf, appIconStart,false);
+ if (filenamesCode.size() > 0) {
+ foundName = filenamesCode.get(0);
+ }
+ ArrayList filenamesIcons = parseAppFilenames(buf, layout_start,false);
+ ArrayList filenamesLayout = parseAppFilenames(buf, display_name_start,true);
+ ArrayList filenamesDisplayName = parseAppFilenames(buf, config_start,true);
+ if (filenamesDisplayName.contains("theme_class")) {
+ isApp = false;
+ isWatchface = true;
+ }
+ }
+
+ private ArrayList parseAppFilenames(ByteBuffer buf, int untilPosition, boolean cutTrailingNull) {
+ ArrayList list = new ArrayList<>();
+ while (buf.position() < untilPosition) {
+ int filenameLength = (int)buf.get();
+ byte[] filenameBytes = new byte[filenameLength - 1];
+ buf.get(filenameBytes);
+ buf.get();
+ list.add(new String(filenameBytes, Charset.forName("UTF8")));
+ int filesize = buf.getShort();
+ if (cutTrailingNull) {
+ filesize -= 1;
+ }
+ buf.position(buf.position() + filesize); // skip file data for now
+ if (cutTrailingNull) {
+ buf.get();
+ }
+ }
+ return list;
+ }
+
+ public boolean isValid() {
+ return isValid;
+ }
+
+ public boolean isFirmware() {
+ return isFirmware;
+ }
+
+ public boolean isApp() {
+ return isApp;
+ }
+
+ public boolean isWatchface() {
+ return isWatchface;
+ }
+
+ public String getVersion() {
+ return foundVersion;
+ }
+
+ public String getName() {
+ return foundName;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilHRInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilHRInstallHandler.java
index 8b5359747..9445df48f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilHRInstallHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilHRInstallHandler.java
@@ -19,11 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.content.Context;
import android.net.Uri;
-import java.io.BufferedInputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
@@ -31,50 +27,19 @@ import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
-import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
public class FossilHRInstallHandler implements InstallHandler {
+ private final Uri mUri;
private final Context mContext;
- private boolean mIsValid;
- private String mVersion = "(Unknown version)";
+ private FossilFileReader fossilFile;
FossilHRInstallHandler(Uri uri, Context context) {
+ mUri = uri;
mContext = context;
- UriHelper uriHelper;
try {
- uriHelper = UriHelper.get(uri, mContext);
- } catch (IOException e) {
- mIsValid = false;
- return;
+ fossilFile = new FossilFileReader(uri, mContext);
+ } catch (IOException ignored) {
}
- try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
- byte[] bytes = new byte[32];
- int read = in.read(bytes);
- if (read < 32) {
- mIsValid = false;
- return;
- }
-
- ByteBuffer buf = ByteBuffer.wrap(bytes);
- buf.order(ByteOrder.LITTLE_ENDIAN);
- int header0 = buf.getInt();
- buf.getInt(); // size
- int header2 = buf.getInt();
- int header3 = buf.getInt();
- if (header0 != 1 || header2 != 0x00012000 || header3 != 0x00012000) {
- mIsValid = false;
- return;
- }
-
- buf.getInt(); // unknown
- int version1 = buf.get() % 0xff;
- int version2 = buf.get() & 0xff;
- mVersion = "DN1.0." + version1 + "." + version2;
- } catch (Exception e) {
- mIsValid = false;
- return;
- }
- mIsValid = true;
}
@Override
@@ -84,17 +49,32 @@ public class FossilHRInstallHandler implements InstallHandler {
installActivity.setInstallEnabled(false);
return;
}
- if (device.getType() != DeviceType.FOSSILQHYBRID || !device.isConnected()) {
+ if (device.getType() != DeviceType.FOSSILQHYBRID || !device.isConnected() || !fossilFile.isValid()) {
installActivity.setInfoText("Element cannot be installed");
installActivity.setInstallEnabled(false);
return;
}
GenericItem installItem = new GenericItem();
- installItem.setIcon(R.drawable.ic_firmware);
- installItem.setName("Fossil Hybrid HR Firmware");
- installItem.setDetails(mVersion);
-
- installActivity.setInfoText(mContext.getString(R.string.firmware_install_warning, "(unknown)"));
+ if (fossilFile.isFirmware()) {
+ installItem.setIcon(R.drawable.ic_firmware);
+ installItem.setName(fossilFile.getName());
+ installItem.setDetails(fossilFile.getVersion());
+ installActivity.setInfoText(mContext.getString(R.string.firmware_install_warning, "(unknown)"));
+ } else if (fossilFile.isApp()) {
+ installItem.setName(fossilFile.getName());
+ installItem.setDetails(fossilFile.getVersion());
+ installItem.setIcon(R.drawable.ic_watchapp);
+ installActivity.setInfoText(mContext.getString(R.string.app_install_info, installItem.getName(), fossilFile.getVersion(), "(unknown)"));
+ } else if (fossilFile.isWatchface()) {
+ installItem.setName(fossilFile.getName());
+ installItem.setDetails(fossilFile.getVersion());
+ installItem.setIcon(R.drawable.ic_watchface);
+ installActivity.setInfoText(mContext.getString(R.string.watchface_install_info, installItem.getName(), fossilFile.getVersion(), "(unknown)"));
+ } else {
+ installActivity.setInfoText("Element cannot be installed");
+ installActivity.setInstallEnabled(false);
+ return;
+ }
installActivity.setInstallEnabled(true);
installActivity.setInstallItem(installItem);
}
@@ -106,6 +86,6 @@ public class FossilHRInstallHandler implements InstallHandler {
@Override
public boolean isValid() {
- return mIsValid;
+ return fossilFile.isValid();
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java
index b071bce9a..de98296a0 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java
@@ -28,6 +28,9 @@ import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.util.Collection;
import java.util.Collections;
import java.util.regex.Matcher;
@@ -48,6 +51,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.Version;
public class QHybridCoordinator extends AbstractDeviceCoordinator {
+ private static final Logger LOG = LoggerFactory.getLogger(QHybridCoordinator.class);
+
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
@@ -103,7 +108,12 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
public InstallHandler findInstallHandler(Uri uri, Context context) {
if (isHybridHR()) {
FossilHRInstallHandler installHandler = new FossilHRInstallHandler(uri, context);
- return installHandler.isValid() ? installHandler : null;
+ if (!installHandler.isValid()) {
+ LOG.warn("Not a Fossil Hybrid firmware or app!");
+ return null;
+ } else {
+ return installHandler;
+ }
}
FossilInstallHandler installHandler = new FossilInstallHandler(uri, context);
return installHandler.isValid() ? installHandler : null;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java
index 0aa001121..784532bba 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java
@@ -27,6 +27,7 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
+import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
@@ -38,11 +39,13 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -66,6 +69,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.CommuteActionsActivity;
+import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilFileReader;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration;
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample;
@@ -127,6 +131,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.mis
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
import nodomain.freeyourgadget.gadgetbridge.util.Version;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.music.MusicControlRequest.MUSIC_PHONE_REQUEST;
@@ -692,32 +697,10 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
@Override
public void uploadFileIncludesHeader(String filePath) {
final Intent resultIntent = new Intent(QHybridSupport.QHYBRID_ACTION_UPLOADED_FILE);
- byte[] fileData;
-
try {
FileInputStream fis = new FileInputStream(filePath);
- fileData = new byte[fis.available()];
- fis.read(fileData);
+ uploadFileIncludesHeader(fis);
fis.close();
-
- short handleBytes = (short) (fileData[0] & 0xFF | ((fileData[1] & 0xFF) << 8));
- FileHandle handle = FileHandle.fromHandle(handleBytes);
-
- if (handle == null) {
- throw new RuntimeException("unknown handle");
- }
-
- queueWrite(new FilePutRawRequest(handle, fileData, this) {
- @Override
- public void onFilePut(boolean success) {
- resultIntent.putExtra("EXTRA_SUCCESS", success);
- LocalBroadcastManager.getInstance(getContext()).sendBroadcast(resultIntent);
- }
- });
-
- if (handle == FileHandle.APP_CODE) {
- listApplications();
- }
} catch (Exception e) {
LOG.error("Error while uploading file", e);
resultIntent.putExtra("EXTRA_SUCCESS", false);
@@ -725,6 +708,31 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
}
}
+ private void uploadFileIncludesHeader(InputStream fis) throws IOException {
+ final Intent resultIntent = new Intent(QHybridSupport.QHYBRID_ACTION_UPLOADED_FILE);
+ byte[] fileData = new byte[fis.available()];
+ fis.read(fileData);
+
+ short handleBytes = (short) (fileData[0] & 0xFF | ((fileData[1] & 0xFF) << 8));
+ FileHandle handle = FileHandle.fromHandle(handleBytes);
+
+ if (handle == null) {
+ throw new RuntimeException("unknown handle");
+ }
+
+ queueWrite(new FilePutRawRequest(handle, fileData, this) {
+ @Override
+ public void onFilePut(boolean success) {
+ resultIntent.putExtra("EXTRA_SUCCESS", success);
+ LocalBroadcastManager.getInstance(getContext()).sendBroadcast(resultIntent);
+ }
+ });
+
+ if (handle == FileHandle.APP_CODE) {
+ listApplications();
+ }
+ }
+
@Override
public void downloadFile(final FileHandle handle, boolean fileIsEncrypted) {
if (fileIsEncrypted) {
@@ -776,6 +784,24 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
}
}
+ @Override
+ public void onInstallApp(Uri uri) {
+ final Intent resultIntent = new Intent(QHybridSupport.QHYBRID_ACTION_UPLOADED_FILE);
+ FossilFileReader fossilFile;
+ try {
+ fossilFile = new FossilFileReader(uri, getContext());
+ if (fossilFile.isFirmware()) {
+ super.onInstallApp(uri);
+ } else if (fossilFile.isApp() || fossilFile.isWatchface()) {
+ UriHelper uriHelper = UriHelper.get(uri, getContext());
+ InputStream in = new BufferedInputStream(uriHelper.openInputStream());
+ uploadFileIncludesHeader(in);
+ in.close();
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
private void negotiateSymmetricKey() {
try {
queueWrite(new VerifyPrivateKeyRequest(
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e4d82a961..9f7c87cc6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -344,7 +344,8 @@
Installation failed
Installed
YOU ARE TRYING TO INSTALL A FIRMWARE, PROCEED AT YOUR OWN RISK.\n\n\n This firmware is for HW Revision: %s
- You are about to install the following app:\n\n\n%1$s Version %2$s by %3$s\n
+ You are about to install the following app:\n\n%1$s\nVersion %2$s by %3$s\n
+ You are about to install the following watchface:\n\n%1$s\nVersion %2$s by %3$s\n
N/A
initialized
%1$s by %2$s