1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-29 03:55:49 +01:00

Fossil Hybrid HR: Add watch app and watchface support to install handler

This commit is contained in:
Arjan Schrijver 2021-05-24 21:49:54 +02:00
parent e12f391dd0
commit 9dac0a80c3
5 changed files with 272 additions and 72 deletions

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<String> filenamesCode = parseAppFilenames(buf, appIconStart,false);
if (filenamesCode.size() > 0) {
foundName = filenamesCode.get(0);
}
ArrayList<String> filenamesIcons = parseAppFilenames(buf, layout_start,false);
ArrayList<String> filenamesLayout = parseAppFilenames(buf, display_name_start,true);
ArrayList<String> filenamesDisplayName = parseAppFilenames(buf, config_start,true);
if (filenamesDisplayName.contains("theme_class")) {
isApp = false;
isWatchface = true;
}
}
private ArrayList<String> parseAppFilenames(ByteBuffer buf, int untilPosition, boolean cutTrailingNull) {
ArrayList<String> 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;
}
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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(

View File

@ -344,7 +344,8 @@
<string name="installation_failed_">Installation failed</string>
<string name="installation_successful">Installed</string>
<string name="firmware_install_warning">YOU ARE TRYING TO INSTALL A FIRMWARE, PROCEED AT YOUR OWN RISK.\n\n\n This firmware is for HW Revision: %s</string>
<string name="app_install_info">You are about to install the following app:\n\n\n%1$s Version %2$s by %3$s\n</string>
<string name="app_install_info">You are about to install the following app:\n\n%1$s\nVersion %2$s by %3$s\n</string>
<string name="watchface_install_info">You are about to install the following watchface:\n\n%1$s\nVersion %2$s by %3$s\n</string>
<string name="n_a">N/A</string>
<string name="initialized">initialized</string>
<string name="appversion_by_creator">%1$s by %2$s</string>