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:
parent
e12f391dd0
commit
9dac0a80c3
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user