diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiBRCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiBRCoordinator.java
index 2088c962d..4f56a104a 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiBRCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiBRCoordinator.java
@@ -33,6 +33,7 @@ import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
+import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator;
@@ -170,12 +171,31 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
@Override
public Class extends Activity> getAppsManagementActivity() {
- return null;
+ return huaweiCoordinator.getAppManagerActivity();
}
+ @Override
+ public boolean supportsAppListFetching() {
+ return huaweiCoordinator.getSupportsAppListFetching();
+ }
@Override
public boolean supportsAppsManagement(GBDevice device) {
- return false;
+ return huaweiCoordinator.getSupportsAppsManagement(device);
+ }
+
+ @Override
+ public boolean supportsWatchfaceManagement(GBDevice device) {
+ return supportsAppsManagement(device);
+ }
+
+ @Override
+ public boolean supportsInstalledAppManagement(GBDevice device) {
+ return huaweiCoordinator.getSupportsInstalledAppManagement(device);
+ }
+
+ @Override
+ public boolean supportsCachedAppManagement(GBDevice device) {
+ return huaweiCoordinator.getSupportsCachedAppManagement(device);
}
@Override
@@ -208,9 +228,10 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
return huaweiCoordinator.supportsMusic();
}
+
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
- return null;
+ return huaweiCoordinator.getInstallHandler(uri, context);
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java
index 9070d0412..92a6fe7db 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java
@@ -16,8 +16,10 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
+import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
+import android.net.Uri;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -30,10 +32,13 @@ import org.slf4j.Logger;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
+import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications.NotificationConstraintsType;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -49,7 +54,11 @@ public class HuaweiCoordinator {
byte notificationCapabilities = -0x01;
ByteBuffer notificationConstraints = null;
+ private Watchface.WatchfaceDeviceParams watchfaceDeviceParams;
+
private final HuaweiCoordinatorSupplier parent;
+
+
private boolean transactionCrypted=true;
public HuaweiCoordinator(HuaweiCoordinatorSupplier parent) {
@@ -75,6 +84,7 @@ public class HuaweiCoordinator {
}
}
+
private SharedPreferences getCapabilitiesSharedPreferences() {
return GBApplication.getContext().getSharedPreferences("huawei_coordinator_capatilities" + parent.getDeviceType().name(), Context.MODE_PRIVATE);
}
@@ -380,6 +390,8 @@ public class HuaweiCoordinator {
return supportsCommandForService(0x0c, 0x01);
}
+ public boolean supportsWatchfaceParams(){ return supportsCommandForService(0x27, 0x01);}
+
public boolean supportsWeather() {
return supportsCommandForService(0x0f, 0x01);
}
@@ -540,5 +552,44 @@ public class HuaweiCoordinator {
"zh_CN",
"zh_TW",
};
+
}
+
+ public short getWidth() {
+ return watchfaceDeviceParams.width;
+ }
+
+ public short getHeight() {
+ return watchfaceDeviceParams.height;
+ }
+
+ public void setWatchfaceDeviceParams(Watchface.WatchfaceDeviceParams watchfaceDeviceParams) {
+ this.watchfaceDeviceParams = watchfaceDeviceParams;
+ }
+
+ public Class extends Activity> getAppManagerActivity() {
+ return AppManagerActivity.class;
+ }
+
+ public boolean getSupportsAppListFetching() {
+ return true;
+ }
+
+ public boolean getSupportsAppsManagement(GBDevice device) {
+ return true;
+ }
+
+ public boolean getSupportsInstalledAppManagement(GBDevice device) {
+ return false;
+ }
+
+ public boolean getSupportsCachedAppManagement(GBDevice device) {
+ return false;
+ }
+
+ public InstallHandler getInstallHandler(Uri uri, Context context) {
+ HuaweiInstallHandler handler = new HuaweiInstallHandler(uri, context);
+ return handler.isValid() ? handler : null;
+ }
+
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiInstallHandler.java
new file mode 100644
index 000000000..78dc64898
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiInstallHandler.java
@@ -0,0 +1,121 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ 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.huawei;
+
+import android.content.Context;
+import android.net.Uri;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
+
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiFwHelper;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWatchfaceManager;
+
+public class HuaweiInstallHandler implements InstallHandler {
+ private static final Logger LOG = LoggerFactory.getLogger(HuaweiInstallHandler.class);
+
+ private final Context context;
+ protected final HuaweiFwHelper helper;
+
+ boolean valid = false;
+
+ public HuaweiInstallHandler(Uri uri, Context context) {
+ this.context = context;
+ this.helper = new HuaweiFwHelper(uri, context);
+ }
+
+
+ @Override
+ public void validateInstallation(InstallActivity installActivity, GBDevice device) {
+
+ final DeviceCoordinator coordinator = device.getDeviceCoordinator();
+ if (!(coordinator instanceof HuaweiCoordinatorSupplier)) {
+ LOG.warn("Coordinator is not a HuaweiCoordinatorSupplier: {}", coordinator.getClass());
+ installActivity.setInstallEnabled(false);
+ return;
+ }
+
+ final HuaweiCoordinatorSupplier huaweiCoordinatorSupplier = (HuaweiCoordinatorSupplier) coordinator;
+
+ HuaweiWatchfaceManager.WatchfaceDescription description = helper.getWatchfaceDescription();
+
+ HuaweiWatchfaceManager.Resolution resolution = new HuaweiWatchfaceManager.Resolution();
+ String deviceScreen = String.format("%d*%d",huaweiCoordinatorSupplier.getHuaweiCoordinator().getHeight(),
+ huaweiCoordinatorSupplier.getHuaweiCoordinator().getWidth());
+ this.valid = resolution.isValid(description.screen, deviceScreen);
+
+ installActivity.setInstallEnabled(true);
+
+ GenericItem installItem = new GenericItem();
+
+
+ if (helper.getWatchfacePreviewBitmap() != null) {
+ installItem.setPreview(helper.getWatchfacePreviewBitmap());
+ }
+
+ installItem.setName(description.title);
+ installActivity.setInstallItem(installItem);
+ if (device.isBusy()) {
+ LOG.error("Firmware cannot be installed (device busy)");
+ installActivity.setInfoText("Firmware cannot be installed (device busy)");
+ installActivity.setInfoText(device.getBusyTask());
+ installActivity.setInstallEnabled(false);
+ return;
+ }
+
+ if ( !device.isConnected()) {
+ LOG.error("Firmware cannot be installed (not connected or wrong device)");
+ installActivity.setInfoText("Firmware cannot be installed (not connected or wrong device)");
+ installActivity.setInstallEnabled(false);
+ return;
+ }
+
+ if (!this.valid) {
+ LOG.error("Watchface cannot be installed");
+ installActivity.setInfoText(context.getString(R.string.watchface_resolution_doesnt_match,
+ resolution.screenByThemeVersion(description.screen), deviceScreen));
+ installActivity.setInstallEnabled(false);
+ return;
+ }
+
+
+ //installItem.setDetails(description.version);
+
+ installItem.setIcon(R.drawable.ic_watchface);
+ installActivity.setInfoText(context.getString(R.string.watchface_install_info, installItem.getName(), description.version, description.author));
+
+ LOG.debug("Initialized HuaweiInstallHandler");
+
+ }
+
+ @Override
+ public boolean isValid() {
+ return helper.isValid();
+ }
+
+ @Override
+ public void onStartInstall(GBDevice device) {
+ helper.unsetFwBytes();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java
index a828fc0be..a7fc8b3a0 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiLECoordinator.java
@@ -14,6 +14,7 @@
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.huawei;
import android.app.Activity;
@@ -32,11 +33,13 @@ import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
+import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@@ -170,12 +173,31 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
@Override
public Class extends Activity> getAppsManagementActivity() {
- return null;
+ return huaweiCoordinator.getAppManagerActivity();
}
+ @Override
+ public boolean supportsAppListFetching() {
+ return huaweiCoordinator.getSupportsAppListFetching();
+ }
@Override
public boolean supportsAppsManagement(GBDevice device) {
- return false;
+ return huaweiCoordinator.getSupportsAppsManagement(device);
+ }
+
+ @Override
+ public boolean supportsWatchfaceManagement(GBDevice device) {
+ return supportsAppsManagement(device);
+ }
+
+ @Override
+ public boolean supportsInstalledAppManagement(GBDevice device) {
+ return huaweiCoordinator.getSupportsInstalledAppManagement(device);
+ }
+
+ @Override
+ public boolean supportsCachedAppManagement(GBDevice device) {
+ return huaweiCoordinator.getSupportsCachedAppManagement(device);
}
@Override
@@ -210,7 +232,7 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
- return null;
+ return huaweiCoordinator.getInstallHandler(uri, context);
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java
index 07f1b8c23..595871814 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java
@@ -33,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Calls;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
@@ -40,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
public class HuaweiPacket {
@@ -539,6 +541,28 @@ public class HuaweiPacket {
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
return this;
}
+ case FileUpload.id:
+ switch(this.commandId) {
+ case FileUpload.FileNextChunkParams.id:
+ return new FileUpload.FileNextChunkParams(paramsProvider).fromPacket(this);
+ case FileUpload.FileUploadConsultAck.id:
+ return new FileUpload.FileUploadConsultAck.Response(paramsProvider).fromPacket(this);
+ default:
+ this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
+ return this;
+ }
+ case Watchface.id:
+ switch (this.commandId) {
+ case Watchface.WatchfaceParams.id:
+ return new Watchface.WatchfaceParams.Response(paramsProvider).fromPacket(this);
+ case Watchface.DeviceWatchInfo.id:
+ return new Watchface.DeviceWatchInfo.Response(paramsProvider).fromPacket(this);
+ case Watchface.WatchfaceNameInfo.id:
+ return new Watchface.WatchfaceNameInfo.Response(paramsProvider).fromPacket(this);
+ default:
+ this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
+ return this;
+ }
default:
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
return this;
@@ -662,6 +686,62 @@ public class HuaweiPacket {
return retv;
}
+ public List serializeFileChunk(byte[] fileChunk, int uploadPosition, short unitSize) {
+ List retv = new ArrayList<>();
+ int headerLength = 5; // Magic + (short)(bodyLength + 1) + 0x00
+ int sliceHeaderLenght =7;
+
+ int footerLength = 2; //CRC16
+
+ int packetCount = (int) Math.ceil(((double) fileChunk.length ) / (double) unitSize);
+
+ ByteBuffer buffer = ByteBuffer.wrap(fileChunk);
+
+ byte fileType = 0x01; //TODO: 1 - watchface, 2 - music
+ int sliceStart = uploadPosition;
+
+ for (int i = 0; i < packetCount; i++) {
+
+ short contentSize = (short) Math.min(unitSize, buffer.remaining());
+ short packetSize = (short)(contentSize + headerLength + sliceHeaderLenght + footerLength);
+ ByteBuffer packet = ByteBuffer.allocate(packetSize);
+
+ int start = packet.position();
+ packet.put((byte) 0x5a); // Magic byte
+ packet.putShort((short) (packetSize - headerLength)); // Length
+
+ packet.put((byte) 0x00);
+ packet.put(this.serviceId);
+ packet.put(this.commandId);
+
+ packet.put(fileType); // Slice
+ packet.put((byte)i); // Flag
+ packet.putInt(sliceStart);
+
+ byte[] packetContent = new byte[contentSize];
+ buffer.get(packetContent);
+ packet.put(packetContent); // Packet databyte[] packetContent = new byte[contentSize];
+
+ int length = packet.position() - start;
+ if (length != packetSize - footerLength) {
+ // TODO: exception?
+ LOG.error(String.format(GBApplication.getLanguage(), "Packet lengths don't match! %d != %d", length, packetSize + headerLength));
+ }
+
+ byte[] complete = new byte[length];
+ packet.position(start);
+ packet.get(complete, 0, length);
+ int crc16 = CheckSums.getCRC16(complete, 0x0000);
+
+ packet.putShort((short) crc16); // CRC16
+
+ sliceStart += contentSize;
+
+ retv.add(packet.array());
+ }
+ return retv;
+ }
+
public List serialize() throws CryptoException {
// TODO: necessary for this to work:
// - serviceId
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FileUpload.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FileUpload.java
new file mode 100644
index 000000000..137725590
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FileUpload.java
@@ -0,0 +1,203 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ 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.huawei.packets;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
+
+public class FileUpload {
+ public static final byte id = 0x28;
+
+ public static class FileUploadParams {
+ public byte file_id = 0;
+ public String protocolVersion = "";
+ public short app_wait_time = 0;
+ public byte bitmap_enable = 0;
+ public short unit_size = 0;
+ public int max_apply_data_size = 0;
+ public short interval =0;
+ public int received_file_size =0;
+ public byte no_encrypt = 0;
+ }
+
+ public static class Filetype {
+ public static final byte watchface = 1;
+ public static final byte music = 2;
+ public static final byte backgroundImage = 3;
+ public static final byte app = 7;
+ }
+
+
+ public static class FileInfoSend {
+ public static final byte id = 0x02;
+ public static class Request extends HuaweiPacket {
+
+ public Request(ParamsProvider paramsProvider,
+ int fileSize,
+ String fileName,
+ byte fileType) {
+ super(paramsProvider);
+ this.serviceId = FileUpload.id;
+ this.commandId = id;
+
+ this.tlv = new HuaweiTLV()
+ .put(0x01, fileName)
+ .put(0x02, fileSize)
+ .put(0x03, (byte) fileType);
+
+ if (fileType == Filetype.watchface) {
+ String watchfaceName = fileName.split("_")[0];
+ String watchfaceVersion = fileName.split("_")[1];
+ this.tlv.put(0x05, watchfaceName)
+ .put(0x06, watchfaceVersion);
+ }
+
+ this.complete = true;
+ }
+ }
+
+ public static class Response extends HuaweiPacket {
+ public Response (ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ }
+ }
+ }
+
+ public static class FileHashSend {
+ public static final byte id = 0x03;
+
+ public static class Request extends HuaweiPacket {
+
+ public Request(ParamsProvider paramsProvider,
+ byte[] hash,
+ byte fileType) {
+ super(paramsProvider);
+ this.serviceId = FileUpload.id;
+ this.commandId = id;
+
+ this.tlv = new HuaweiTLV()
+ .put(0x01, fileType)
+ .put(0x03, hash);
+ this.complete = true;
+ }
+ }
+
+ public static class Response extends HuaweiPacket {
+ public Response (ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ }
+ }
+ }
+
+ public static class FileUploadConsultAck {
+ public static final byte id = 0x04;
+ public static class Request extends HuaweiPacket {
+ public Request(ParamsProvider paramsProvider, byte noEncryption, byte fileType) {
+ super(paramsProvider);
+ this.serviceId = FileUpload.id;
+ this.commandId = id;
+ this.tlv = new HuaweiTLV()
+ .put(0x7f, 0x000186A0) //ok
+ .put(0x01, fileType);
+ if (noEncryption == 1)
+ this.tlv.put(0x09, (byte)0x01); // need on devices which generally encrypted, but files
+ this.complete = true;
+ }
+ }
+
+ public static class Response extends HuaweiPacket {
+
+ public FileUploadParams fileUploadParams = new FileUploadParams();
+ public Response (ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ }
+
+ @Override
+ public void parseTlv() throws HuaweiPacket.ParseException {
+ this.fileUploadParams.file_id = this.tlv.getByte(0x01);
+ this.fileUploadParams.protocolVersion = this.tlv.getString(0x02);
+ this.fileUploadParams.app_wait_time = this.tlv.getShort(0x03);
+ this.fileUploadParams.bitmap_enable = this.tlv.getByte(0x04);
+ this.fileUploadParams.unit_size = this.tlv.getShort(0x05);
+ this.fileUploadParams.max_apply_data_size = this.tlv.getInteger(0x06);
+ this.fileUploadParams.interval = this.tlv.getShort(0x07);
+ this.fileUploadParams.received_file_size = this.tlv.getInteger(0x08);
+ if (this.tlv.contains(0x09)) // optional for older devices
+ this.fileUploadParams.no_encrypt = this.tlv.getByte(0x09);
+ }
+ }
+ }
+
+ public static class FileNextChunkParams extends HuaweiPacket {
+ public static final byte id = 0x05;
+
+ public int bytesUploaded = 0;
+ public int nextchunkSize = 0;
+ public FileNextChunkParams(ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ this.serviceId = FileUpload.id;
+ this.commandId = id;
+ this.complete = true;
+ }
+ @Override
+ public void parseTlv() throws HuaweiPacket.ParseException {
+ this.bytesUploaded = this.tlv.getInteger(0x02);
+ this.nextchunkSize = this.tlv.getInteger(0x03);
+ }
+ }
+
+ public static class FileNextChunkSend extends HuaweiPacket {
+ public static final byte id = 0x06;
+
+ public FileNextChunkSend(ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ this.serviceId = FileUpload.id;
+ this.commandId = id;
+ this.complete = true;
+ }
+ }
+
+ public static class FileUploadResult {
+ public static final byte id = 0x07;
+
+ public static class Request extends HuaweiPacket {
+ public Request(ParamsProvider paramsProvider, byte fileType) {
+ super(paramsProvider);
+ this.serviceId = FileUpload.id;
+ this.commandId = id;
+ this.tlv = new HuaweiTLV()
+ .put(0x7f, 0x000186A0) //ok
+ .put(0x01, fileType);
+
+ this.complete = true;
+ }
+ }
+
+ public static class Response extends HuaweiPacket {
+ byte status = 0;
+ public Response (ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ }
+
+ @Override
+ public void parseTlv() throws HuaweiPacket.ParseException {
+ this.status = this.tlv.getByte(0x02);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Watchface.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Watchface.java
new file mode 100644
index 000000000..39b0aaf8b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Watchface.java
@@ -0,0 +1,247 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ 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.huawei.packets;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
+
+public class Watchface {
+ public static final byte id = 0x27;
+
+ public static class WatchfaceDeviceParams {
+ public String maxVersion = "";
+ public short width = 0;
+ public short height = 0;
+ public byte supportFileType = 1;
+ public byte sort = 1;
+ public String otherWatchfaceVersions = "";
+ }
+
+ public static class InstalledWatchfaceInfo {
+ public String fileName = "";
+ public String version = "";
+ public byte type = 0;
+ // bit 0 - is current
+ // bit 1 - is factory preset
+ // bit 2 - ???
+ // bit 3 - editable
+ // bit 4 - video
+ // bit 5 - photo
+ // bit 6 - tryout (trial version)
+ // bit 7 - kaleidoskop
+ public byte expandedtype = 0;
+
+ public InstalledWatchfaceInfo(HuaweiTLV tlv) throws HuaweiPacket.MissingTagException {
+ this.fileName = tlv.getString(0x03);
+ this.version = tlv.getString(0x04);
+ this.type = tlv.getByte(0x05);
+ if (tlv.contains(0x07)) // optional
+ this.expandedtype = tlv.getByte(0x07);
+ }
+
+ public boolean isCurrent() {
+ return (this.type & 1) == 1;
+ }
+ public boolean isFactory() {
+ return ((this.type >> 1 )& 1) == 1;
+ }
+ public boolean isEditable() {
+ return ((this.type >> 3 )& 1) == 1;
+ }
+ public boolean isVideo() {
+ return ((this.type >> 4 )& 1) == 1;
+ }
+ public boolean isPhoto() {
+ return ((this.type >> 5 )& 1) == 1;
+ }
+ public boolean isTryout() {
+ return ((this.type >> 6 )& 1) == 1;
+ }
+ public boolean isKaleidoskop() {
+ return ((this.type >> 7 )& 1) == 1;
+ }
+ }
+
+ public static class WatchfaceParams {
+ public static final byte id = 0x01;
+ public static class Request extends HuaweiPacket {
+
+ public Request(ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ this.serviceId = Watchface.id;
+ this.commandId = id;
+
+ this.tlv = new HuaweiTLV()
+ .put(0x01)
+ .put(0x02)
+ .put(0x03)
+ .put(0x04)
+ .put(0x05)
+ .put(0x0e)
+ .put(0x0f);
+
+ this.complete = true;
+ }
+ }
+
+ public static class Response extends HuaweiPacket {
+ public WatchfaceDeviceParams params = new WatchfaceDeviceParams();
+ public Response (ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ }
+
+ @Override
+ public void parseTlv() throws ParseException {
+ this.params.maxVersion = this.tlv.getString(0x01);
+ this.params.width = this.tlv.getShort(0x02);
+ this.params.height = this.tlv.getShort(0x03);
+ this.params.supportFileType = this.tlv.getByte(0x04);
+ this.params.sort = this.tlv.getByte(0x05);
+ this.params.otherWatchfaceVersions = this.tlv.getString(0x06);
+ }
+ }
+ }
+
+ public static class DeviceWatchInfo {
+ public static final byte id = 0x02;
+
+ public static class Request extends HuaweiPacket {
+ public Request(ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ this.serviceId = Watchface.id;
+ this.commandId = id;
+ this.tlv = new HuaweiTLV()
+ .put(0x01)
+ .put(0x06, (byte) 0x03); //3 -overseas non-test, 2 - test, 1 -null?
+ }
+ }
+
+ public static class Response extends HuaweiPacket {
+
+ public List watchfaceInfoList;
+ public Response (ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ }
+
+ @Override
+ public void parseTlv() throws HuaweiPacket.ParseException {
+ watchfaceInfoList = new ArrayList<>();
+ if(this.tlv.contains(0x81)) {
+ for (HuaweiTLV subTlv : this.tlv.getObject(0x81).getObjects(0x82)) {
+ watchfaceInfoList.add(new Watchface.InstalledWatchfaceInfo(subTlv));
+ }
+ }
+ }
+ }
+ }
+
+ public static class WatchfaceOperation {
+ public static final byte id = 0x03;
+
+ public static final byte operationActive = 1;
+ public static final byte operationDelete = 2;
+
+ public static class Request extends HuaweiPacket {
+ public Request(ParamsProvider paramsProvider,
+ String fileName, byte operation) {
+ super(paramsProvider);
+ this.serviceId = Watchface.id;
+ this.tlv = new HuaweiTLV()
+ .put(0x01, fileName.split("_")[0])
+ .put(0x02, fileName.split("_")[1])
+ .put(0x03, operation);
+
+ this.commandId = id;
+ }
+ }
+
+ public static class Response extends HuaweiPacket {
+ public Response (ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ }
+ }
+ }
+
+ public static class WatchfaceConfirm {
+ public static final byte id = 0x05;
+
+ public static class Request extends HuaweiPacket {
+ public Request(ParamsProvider paramsProvider,
+ String fileName) {
+ super(paramsProvider);
+ this.serviceId = Watchface.id;
+ this.commandId = id;
+ this.tlv = new HuaweiTLV()
+ .put(0x01, fileName.split("_")[0])
+ .put(0x02, fileName.split("_")[1])
+ .put(0x7f, 0x000186A0);
+ }
+ }
+
+ public static class Response extends HuaweiPacket {
+ public Response (ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ }
+ }
+ }
+
+ public static class WatchfaceNameInfo {
+ public static final byte id = 0x06;
+
+ public static class Request extends HuaweiPacket {
+ public Request(ParamsProvider paramsProvider,
+ List watchfaceList) {
+ super(paramsProvider);
+ this.serviceId = Watchface.id;
+ this.commandId = id;
+
+ HuaweiTLV tlvList = new HuaweiTLV();
+ for (InstalledWatchfaceInfo watchface : watchfaceList) {
+ //TODO: ask name only for custom watchfaces
+ HuaweiTLV wfTlv = new HuaweiTLV().put(0x04, watchface.fileName);
+ tlvList.put(0x83, wfTlv);
+ }
+
+ this.tlv = new HuaweiTLV()
+ .put(0x01, (byte) 0x01)
+ .put(0x82, tlvList);
+
+ }
+ }
+
+ public static class Response extends HuaweiPacket {
+ public HashMap watchFaceNames = new HashMap();
+ public Response (ParamsProvider paramsProvider) {
+ super(paramsProvider);
+ }
+
+ @Override
+ public void parseTlv() throws HuaweiPacket.ParseException {
+ if(this.tlv.contains(0x82)) {
+ for (HuaweiTLV subTlv : this.tlv.getObject(0x82).getObjects(0x83)) {
+ watchFaceNames.put(subTlv.getString(0x04), subTlv.getString(0x05));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java
index 490eb2152..edbed55ff 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java
@@ -48,10 +48,18 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetPhoneInfoRequest;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadComplete;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendMenstrualModifyTimeRequest;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadAck;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadChunk;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadHash;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWatchfaceConfirm;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWatchfaceOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherDeviceRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetMusicStatusRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -100,6 +108,8 @@ public class AsynchronousResponse {
handleMenstrualModifyTime(response);
handleWeatherCheck(response);
handleGpsRequest(response);
+ handleFileUpload(response);
+ handleWatchface(response);
} catch (Request.ResponseParseException e) {
LOG.error("Response parse exception", e);
}
@@ -384,6 +394,79 @@ public class AsynchronousResponse {
}
}
+ private void handleFileUpload(HuaweiPacket response) throws Request.ResponseParseException {
+ if (response.serviceId == FileUpload.id) {
+ if (response.commandId == FileUpload.FileHashSend.id) {
+ try {
+ SendFileUploadHash sendFileUploadHash = new SendFileUploadHash(support, support.huaweiUploadManager);
+ sendFileUploadHash.doPerform();
+ } catch (IOException e) {
+ LOG.error("Could not send fileupload hash request", e);
+ }
+ } else if (response.commandId == FileUpload.FileUploadConsultAck.id) {
+ if (!(response instanceof FileUpload.FileUploadConsultAck.Response))
+ throw new Request.ResponseTypeMismatchException(response, FileUpload.FileUploadConsultAck.Response.class);
+ FileUpload.FileUploadConsultAck.Response resp = (FileUpload.FileUploadConsultAck.Response) response;
+
+ support.huaweiUploadManager.setFileUploadParams(resp.fileUploadParams);
+
+ try {
+ support.huaweiUploadManager.setDeviceBusy();
+ SendFileUploadAck sendFileUploadAck = new SendFileUploadAck(support,
+ resp.fileUploadParams.no_encrypt, support.huaweiUploadManager.getFileType());
+ sendFileUploadAck.doPerform();
+ } catch (IOException e) {
+ LOG.error("Could not send fileupload ack request", e);
+ }
+ } else if (response.commandId == FileUpload.FileNextChunkParams.id) {
+ if (!(response instanceof FileUpload.FileNextChunkParams))
+ throw new Request.ResponseTypeMismatchException(response, FileUpload.FileNextChunkParams.class);
+ FileUpload.FileNextChunkParams resp = (FileUpload.FileNextChunkParams) response;
+ support.huaweiUploadManager.setUploadChunkSize(resp.nextchunkSize);
+ support.huaweiUploadManager.setCurrentUploadPosition(resp.bytesUploaded);
+ int progress = Math.round(((float)resp.bytesUploaded / (float)support.huaweiUploadManager.getFileSize())* 100);
+ support.onUploadProgress(R.string.updatefirmwareoperation_update_in_progress, progress, true);
+
+ try {
+ SendFileUploadChunk sendFileUploadChunk = new SendFileUploadChunk(support, support.huaweiUploadManager);
+ sendFileUploadChunk.doPerform();
+ } catch (IOException e) {
+ LOG.error("Could not send fileupload next chunk request", e);
+ }
+ } else if (response.commandId == FileUpload.FileUploadResult.id) {
+ try {
+ support.huaweiUploadManager.unsetDeviceBusy();
+ support.onUploadProgress(R.string.updatefirmwareoperation_update_complete, 100, false);
+ SendFileUploadComplete sendFileUploadComplete = new SendFileUploadComplete(this.support, support.huaweiUploadManager.getFileType());
+ sendFileUploadComplete.doPerform();
+ if (support.huaweiUploadManager.getFileType() == FileUpload.Filetype.watchface) {
+ //make uploaded watchface active
+ SendWatchfaceOperation sendWatchfaceOperation = new SendWatchfaceOperation(this.support, support.huaweiUploadManager.getFileName(), Watchface.WatchfaceOperation.operationActive);
+ sendWatchfaceOperation.doPerform();
+ }
+ } catch (IOException e) {
+ LOG.error("Could not send fileupload result request", e);
+ }
+ }
+ }
+ }
+
+ private void handleWatchface(HuaweiPacket response) throws Request.ResponseParseException {
+ if (response.serviceId == Watchface.id) {
+ if (response.commandId == Watchface.WatchfaceConfirm.id) {
+ try {
+ SendWatchfaceConfirm sendWatchfaceConfirm = new SendWatchfaceConfirm(this.support, this.support.huaweiUploadManager.getFileName());
+ sendWatchfaceConfirm.doPerform();
+
+
+ } catch (IOException e) {
+ LOG.error("Could not send watchface confirm request", e);
+ }
+
+ }
+ }
+ }
+
private void handleWeatherCheck(HuaweiPacket response) {
if (response.serviceId == Weather.id && response.commandId == 0x04) {
// Send back ok
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java
index 394acbef4..3c2bd15c4 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java
@@ -17,10 +17,12 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
import android.location.Location;
+import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
+import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
@@ -130,4 +132,27 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
public void onSetGpsLocation(Location location) {
supportProvider.onSetGpsLocation(location);
}
+
+ @Override
+ public void onInstallApp(Uri uri) {
+ supportProvider.onInstallApp(uri);
+ }
+
+ @Override
+ public void onAppInfoReq() {
+ supportProvider.onAppInfoReq();
+ }
+
+ @Override
+ public void onAppStart(final UUID uuid, boolean start) {
+ if (start) {
+ supportProvider.onAppStart(uuid, start);
+ }
+ }
+
+ @Override
+ public void onAppDelete(final UUID uuid) {
+ supportProvider.onAppDelete(uuid);
+ }
+
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFwHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFwHelper.java
new file mode 100644
index 000000000..f1e8427ac
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFwHelper.java
@@ -0,0 +1,135 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
+import nodomain.freeyourgadget.gadgetbridge.util.GBZipFile;
+import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
+import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException;
+
+public class HuaweiFwHelper {
+ private static final Logger LOG = LoggerFactory.getLogger(HuaweiFwHelper.class);
+
+ private final Uri uri;
+
+ private byte[] fw;
+ private int fileSize = 0;
+ private byte fileType = 0;
+ String fileName = "";
+
+ Bitmap watchfacePreviewBitmap;
+ HuaweiWatchfaceManager.WatchfaceDescription watchfaceDescription;
+ Context mContext;
+
+ public HuaweiFwHelper(final Uri uri, final Context context) {
+
+ this.uri = uri;
+ final UriHelper uriHelper;
+ this.mContext = context;
+ try {
+ uriHelper = UriHelper.get(uri, context);
+ } catch (final IOException e) {
+ LOG.error("Failed to get uri helper for {}", uri, e);
+ return;
+ }
+
+ parseFile();
+ }
+
+ private void parseFile() {
+ if (parseAsWatchFace()) {
+ assert watchfaceDescription.screen != null;
+ assert watchfaceDescription.title != null;
+ fileType = FileUpload.Filetype.watchface;
+ }
+ }
+
+ public byte[] getBytes() {
+ return fw;
+ }
+
+ public void unsetFwBytes() {
+ this.fw = null;
+ }
+
+ boolean parseAsWatchFace() {
+ boolean isWatchface = false;
+
+ try {
+ final UriHelper uriHelper = UriHelper.get(uri, this.mContext);
+
+ GBZipFile watchfacePackage = new GBZipFile(uriHelper.openInputStream());
+ String xmlDescription = new String(watchfacePackage.getFileFromZip("description.xml"));
+ watchfaceDescription = new HuaweiWatchfaceManager.WatchfaceDescription(xmlDescription);
+ if (watchfacePackage.fileExists("preview/cover.jpg")) {
+ final byte[] preview = watchfacePackage.getFileFromZip("preview/cover.jpg");
+ watchfacePreviewBitmap = BitmapFactory.decodeByteArray(preview, 0, preview.length);
+ }
+ fw = watchfacePackage.getFileFromZip("com.huawei.watchface");
+ fileSize = fw.length;
+ isWatchface = true;
+
+ } catch (ZipFileException e) {
+ LOG.error("Unable to read watchface file.", e);
+ } catch (FileNotFoundException e) {
+ LOG.error("The watchface file was not found.", e);
+ } catch (IOException e) {
+ LOG.error("General IO error occurred.", e);
+ } catch (Exception e) {
+ LOG.error("Unknown error occurred.", e);
+ }
+
+ return isWatchface;
+ }
+
+ public boolean isWatchface() {
+ return fileType == FileUpload.Filetype.watchface;
+ }
+ public boolean isValid() {
+ return isWatchface();
+ }
+
+ public Bitmap getWatchfacePreviewBitmap() {
+ return watchfacePreviewBitmap;
+ }
+
+ public HuaweiWatchfaceManager.WatchfaceDescription getWatchfaceDescription() {
+ return watchfaceDescription;
+ }
+
+ public byte getFileType() {
+ return fileType;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java
index e86eac511..1775fd230 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java
@@ -19,11 +19,14 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.location.Location;
+import android.net.Uri;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
+import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
@@ -137,4 +140,28 @@ public class HuaweiLESupport extends AbstractBTLEDeviceSupport {
public void onSetGpsLocation(Location location) {
supportProvider.onSetGpsLocation(location);
}
+
+ @Override
+ public void onInstallApp(Uri uri) {
+ supportProvider.onInstallApp(uri);
+ }
+
+ @Override
+ public void onAppInfoReq() {
+ supportProvider.onAppInfoReq();
+ }
+
+ @Override
+ public void onAppStart(final UUID uuid, boolean start) {
+ if (start) {
+ supportProvider.onAppStart(uuid, start);
+ }
+ }
+
+ @Override
+ public void onAppDelete(final UUID uuid) {
+ supportProvider.onAppDelete(uuid);
+ }
+
+
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java
index 3e9db2bab..28ce5b4ee 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java
@@ -20,6 +20,7 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
+import android.net.Uri;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -79,14 +80,17 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
+
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.AcceptAgreementsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetEventAlarmList;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetGpsParameterRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetNotificationConstraintsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSmartAlarmList;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWatchfaceParams;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendExtendedAccountRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsAndTimeToDeviceRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsDataRequest;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherCurrentRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyHeartRateCapabilityRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyRestHeartRateCapabilityRequest;
@@ -178,6 +182,9 @@ public class HuaweiSupportProvider {
private final HuaweiPacket.ParamsProvider paramsProvider = new HuaweiPacket.ParamsProvider();
protected ResponseManager responseManager = new ResponseManager(this);
+ protected HuaweiUploadManager huaweiUploadManager = new HuaweiUploadManager(this);
+
+ protected HuaweiWatchfaceManager huaweiWatchfaceManager = new HuaweiWatchfaceManager(this);
public HuaweiCoordinatorSupplier getCoordinator() {
return ((HuaweiCoordinatorSupplier) this.gbDevice.getDeviceCoordinator());
@@ -186,6 +193,9 @@ public class HuaweiSupportProvider {
public HuaweiCoordinator getHuaweiCoordinator() {
return getCoordinator().getHuaweiCoordinator();
}
+ public HuaweiWatchfaceManager getHuaweiWatchfaceManager() {
+ return huaweiWatchfaceManager;
+ }
public HuaweiSupportProvider(HuaweiBRSupport support) {
this.brSupport = support;
@@ -725,6 +735,11 @@ public class HuaweiSupportProvider {
GetNotificationConstraintsRequest getNotificationConstraintsReq = new GetNotificationConstraintsRequest(this);
getNotificationConstraintsReq.doPerform();
}
+
+ if (getHuaweiCoordinator().supportsWatchfaceParams()) {
+ GetWatchfaceParams getWatchfaceParams = new GetWatchfaceParams(this);
+ getWatchfaceParams.doPerform();
+ }
} catch (IOException e) {
GB.toast(getContext(), "Initialize dynamic services of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR,
e);
@@ -1822,4 +1837,66 @@ public class HuaweiSupportProvider {
LOG.error("Failed to send GPS data", e);
}
}
+
+ public void onInstallApp(Uri uri) {
+ LOG.info("enter onAppInstall uri: "+uri);
+ HuaweiFwHelper huaweiFwHelper = new HuaweiFwHelper(uri, getContext());
+ huaweiUploadManager.setBytes(huaweiFwHelper.getBytes());
+ huaweiUploadManager.setFileType(huaweiFwHelper.getFileType());
+ if (huaweiFwHelper.isWatchface()) {
+ huaweiUploadManager.setFileName(huaweiWatchfaceManager.getRandomName());
+ } else {
+ huaweiUploadManager.setFileName(huaweiFwHelper.getFileName());
+ }
+
+ try {
+ SendFileUploadInfo sendFileUploadInfo = new SendFileUploadInfo(this, huaweiUploadManager);
+ sendFileUploadInfo.doPerform();
+ } catch (IOException e) {
+ GB.toast(context, "Failed to send file upload info", Toast.LENGTH_SHORT, GB.ERROR, e);
+ LOG.error("Failed to send file upload info", e);
+ }
+ }
+
+ public void onUploadProgress(int textRsrc, int progressPercent, boolean ongoing) {
+ try {
+ if (isBLE()) {
+ nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder leBuilder = createLeTransactionBuilder("FetchRecordedData");
+ leBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction(
+ context.getString(textRsrc),
+ ongoing,
+ progressPercent,
+ context
+ ));
+ leBuilder.queue(leSupport.getQueue());
+ } else {
+ nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder brBuilder = createBrTransactionBuilder("FetchRecordedData");
+ brBuilder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetProgressAction(
+ context.getString(textRsrc),
+ ongoing,
+ progressPercent,
+ context));
+ brBuilder.queue(brSupport.getQueue());
+
+ }
+
+ } catch (final Exception e) {
+ LOG.error("Failed to update progress notification", e);
+ }
+ }
+
+ public void onAppInfoReq() {
+ huaweiWatchfaceManager.requestWatchfaceList();
+ }
+
+ public void onAppStart(final UUID uuid, boolean start) {
+ if (start) {
+ huaweiWatchfaceManager.setWatchface(uuid);
+ }
+ }
+
+ public void onAppDelete(final UUID uuid) {
+ huaweiWatchfaceManager.deleteWatchface(uuid);
+ }
+
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiUploadManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiUploadManager.java
new file mode 100644
index 000000000..4aa12abdc
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiUploadManager.java
@@ -0,0 +1,149 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import android.net.Uri;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload.FileUploadParams;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.GBZipFile;
+import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
+import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException;
+
+public class HuaweiUploadManager {
+ private static final Logger LOG = LoggerFactory.getLogger(HuaweiUploadManager.class);
+ private final HuaweiSupportProvider support;
+ byte[] fileBin;
+ byte[] fileSHA256;
+ byte fileType = 1; // 1 - watchface, 2 - music, 3 - png for background , 7 - app
+ int fileSize = 0;
+
+ int currentUploadPosition = 0;
+ int uploadChunkSize =0;
+
+ String fileName = ""; //FIXME generate random name
+
+ //ack values set from 28 4 response
+ FileUploadParams fileUploadParams;
+
+
+ public HuaweiUploadManager(HuaweiSupportProvider support) {
+ this.support=support;
+ }
+
+ public void setBytes(byte[] uploadArray) {
+
+ this.fileSize = uploadArray.length;
+ this.fileBin = uploadArray;
+
+ try {
+ MessageDigest m = MessageDigest.getInstance("SHA256");
+ m.update(fileBin, 0, fileBin.length);
+ fileSHA256 = m.digest();
+ } catch (NoSuchAlgorithmException e) {
+ LOG.error("Digest alghoritm not found.", e);
+ return;
+ }
+
+ currentUploadPosition = 0;
+ uploadChunkSize = 0;
+
+ LOG.info("File ready for upload, SHA256: "+ GB.hexdump(fileSHA256) + " fileName: " + fileName + " filetype: ", fileType);
+
+ }
+
+ public int getFileSize() {
+ return fileSize;
+ }
+
+ public String getFileName() {
+ return this.fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public byte getFileType() {
+ return this.fileType;
+ }
+
+ public void setFileType(byte fileType) {
+ this.fileType = fileType;
+ }
+
+ public byte[] getFileSHA256() {
+ return fileSHA256;
+ }
+
+ public void setUploadChunkSize(int chunkSize) {
+ uploadChunkSize = chunkSize;
+ }
+
+ public void setCurrentUploadPosition (int pos) {
+ currentUploadPosition = pos;
+ }
+
+ public int getCurrentUploadPosition() {
+ return currentUploadPosition;
+ }
+
+ public byte[] getCurrentChunk() {
+ byte[] ret = new byte[uploadChunkSize];
+ System.arraycopy(fileBin, currentUploadPosition, ret, 0, uploadChunkSize);
+ return ret;
+ }
+
+ public void setFileUploadParams(FileUploadParams params) {
+ this.fileUploadParams = params;
+ }
+
+
+ public short getUnitSize() {
+ return fileUploadParams.unit_size;
+ }
+
+ public void setDeviceBusy() {
+ final GBDevice device = support.getDevice();
+ device.setBusyTask(support.getContext().getString(R.string.uploading_watchface));
+ device.sendDeviceUpdateIntent(support.getContext());
+ }
+
+ public void unsetDeviceBusy() {
+ final GBDevice device = support.getDevice();
+ if (device != null && device.isConnected()) {
+ if (device.isBusy()) {
+ device.unsetBusyTask();
+ device.sendDeviceUpdateIntent(support.getContext());
+ }
+ device.sendDeviceUpdateIntent(support.getContext());
+ }
+ }
+
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWatchfaceManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWatchfaceManager.java
new file mode 100644
index 000000000..13453b6b1
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWatchfaceManager.java
@@ -0,0 +1,303 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface.WatchfaceDeviceParams;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWatchfacesList;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWatchfacesNames;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWatchfaceOperation;
+
+public class HuaweiWatchfaceManager
+{
+ Logger LOG = LoggerFactory.getLogger(HuaweiCoordinator.class);
+
+ public static class Resolution {
+
+ Map map = new HashMap<>();
+ public Resolution() {
+ map.put("HWHD09", "466*466");
+ map.put("HWHD08", "320*320");
+ map.put("HWHD10", "360*320");
+ map.put("HWHD02", "454*454");
+ map.put("HWHD01", "390*390");
+ map.put("HWHD05", "460*188");
+ map.put("HWHD03", "240*120");
+ map.put("HWHD04", "160*80");
+ map.put("HWHD06", "456*280");
+ map.put("HWHD07", "368*194");
+ }
+
+ public boolean isValid(String themeVersion, String screenResolution) {
+ String screen = map.get(themeVersion).toString();
+ if (screenResolution.equals(screen)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public String screenByThemeVersion(String themeVersion) {
+ String screen = map.get(themeVersion).toString();
+ return screen;
+ }
+
+ }
+
+ public static class WatchfaceDescription {
+
+ public String title;
+ public String title_cn;
+ public String author;
+ public String designer;
+ public String screen;
+ public String version;
+ public String font;
+ public String font_cn;
+
+ public WatchfaceDescription(String xmlStr) {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder builder;
+ try {
+ builder = factory.newDocumentBuilder();
+ Document doc = builder.parse(new InputSource(new StringReader(
+ xmlStr)));
+
+ this.title = doc.getElementsByTagName("title").item(0).getTextContent();
+ this.title_cn = doc.getElementsByTagName("title-cn").item(0).getTextContent();
+ this.author = doc.getElementsByTagName("author").item(0).getTextContent();
+ this.designer = doc.getElementsByTagName("designer").item(0).getTextContent();
+ this.screen = doc.getElementsByTagName("screen").item(0).getTextContent();
+ this.version = doc.getElementsByTagName("version").item(0).getTextContent();
+ this.font = doc.getElementsByTagName("font").item(0).getTextContent();
+ this.font_cn = doc.getElementsByTagName("font-cn").item(0).getTextContent();
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private List installedWatchfaceInfoList;
+ private HashMap watchfacesNames;
+
+ private HuaweiSupportProvider support;
+
+ public HuaweiWatchfaceManager(HuaweiSupportProvider support) {
+ this.support = support;
+ }
+
+ public void setInstalledWatchfaceInfoList(List list) {
+ this.installedWatchfaceInfoList = list;
+ }
+
+ public List getInstalledWatchfaceInfoList()
+ {
+ return installedWatchfaceInfoList;
+ }
+
+ public void setWatchfacesNames(HashMap map) {
+ this.watchfacesNames = map;
+ }
+
+
+ public String getRandomName() {
+ Random random = new Random();
+
+ String res="";
+ for (int i = 0; i < 9; i++) {
+ int ran = random.nextInt(9);
+ res += String.valueOf(ran);
+ }
+
+ res += "_1.0.0";
+ return res;
+ }
+
+ public static UUID toWatchfaceUUID(final String id) {
+ // Watchface IDs are numbers as strings - pad them to the right with F
+ // and encode as UUID
+ final String padded = String.format("%-32s", id).replace(' ', 'F');
+ return UUID.fromString(
+ padded.substring(0, 8) + "-" +
+ padded.substring(8, 12) + "-" +
+ padded.substring(12, 16) + "-" +
+ padded.substring(16, 20) + "-" +
+ padded.substring(20, 32)
+ );
+ }
+
+ public static String toWatchfaceId(final UUID uuid) {
+ return uuid.toString()
+ .replaceAll("-", "")
+ .replaceAll("f", "")
+ .replaceAll("F", "");
+ }
+
+ public void handleWatchfaceList() {
+
+ final List gbDeviceApps = new ArrayList<>();
+
+ for (final Watchface.InstalledWatchfaceInfo watchfaceInfo : installedWatchfaceInfoList) {
+ final UUID uuid = toWatchfaceUUID(watchfaceInfo.fileName);
+ GBDeviceApp gbDeviceApp = new GBDeviceApp(
+ uuid,
+ watchfacesNames.get(watchfaceInfo.fileName),
+ "",
+ "",
+ GBDeviceApp.Type.WATCHFACE
+ );
+ gbDeviceApps.add(gbDeviceApp);
+ }
+
+ final GBDeviceEventAppInfo appInfoCmd = new GBDeviceEventAppInfo();
+ appInfoCmd.apps = gbDeviceApps.toArray(new GBDeviceApp[0]);
+ support.evaluateGBDeviceEvent(appInfoCmd);
+ }
+
+ public void updateWatchfaceNames() {
+ Request.RequestCallback finalizeReq = new Request.RequestCallback() {
+ @Override
+ public void call() {
+ handleWatchfaceList();
+ }
+
+ @Override
+ public void handleException(Request.ResponseParseException e) {
+ LOG.error("Watchface update list exception", e);
+ }
+ };
+
+ try {
+ GetWatchfacesNames getWatchfacesNames = new GetWatchfacesNames(support, installedWatchfaceInfoList);
+ getWatchfacesNames.setFinalizeReq(finalizeReq);
+ getWatchfacesNames.doPerform();
+ } catch (IOException e) {
+ LOG.error("Could not get watchface names", e);
+ }
+
+ }
+
+ public void requestWatchfaceList() {
+ Request.RequestCallback finalizeReq = new Request.RequestCallback() {
+ @Override
+ public void call() {
+ updateWatchfaceNames();
+ }
+
+ @Override
+ public void handleException(Request.ResponseParseException e) {
+ LOG.error("Watchface update list exception", e);
+ }
+ };
+
+
+ try {
+ GetWatchfacesList getWatchfacesList = new GetWatchfacesList(support);
+ getWatchfacesList.setFinalizeReq(finalizeReq);
+ getWatchfacesList.doPerform();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ };
+
+ public void setWatchface(UUID uuid) {
+ Request.RequestCallback finalizeReq = new Request.RequestCallback() {
+ @Override
+ public void call() {
+ requestWatchfaceList();
+ }
+
+ @Override
+ public void handleException(Request.ResponseParseException e) {
+ LOG.error("Watchface update list exception", e);
+ }
+ };
+
+ try {
+ SendWatchfaceOperation sendWatchfaceOperation = new SendWatchfaceOperation(support,
+ getFullFileName(uuid),
+ Watchface.WatchfaceOperation.operationActive);
+ sendWatchfaceOperation.setFinalizeReq(finalizeReq);
+ sendWatchfaceOperation.doPerform();
+ } catch (IOException e) {
+ LOG.error("Could not set watchface ", getFullFileName(uuid), e );
+ }
+ }
+
+ public void deleteWatchface(UUID uuid) {
+ Request.RequestCallback finalizeReq = new Request.RequestCallback() {
+ @Override
+ public void call() {
+ requestWatchfaceList();
+ }
+
+ @Override
+ public void handleException(Request.ResponseParseException e) {
+ LOG.error("Watchface update list exception", e);
+ }
+ };
+
+ try {
+ SendWatchfaceOperation sendWatchfaceOperation = new SendWatchfaceOperation(support,
+ getFullFileName(uuid),
+ Watchface.WatchfaceOperation.operationDelete);
+ sendWatchfaceOperation.setFinalizeReq(finalizeReq);
+ sendWatchfaceOperation.doPerform();
+ } catch (IOException e) {
+ LOG.error("Could not delete watchface", getFullFileName(uuid), e);
+ }
+ }
+
+ private String getFullFileName(UUID uuid) {
+ String name = toWatchfaceId(uuid);
+ String version = "";
+ for (final Watchface.InstalledWatchfaceInfo watchfaceInfo : installedWatchfaceInfoList) {
+ if (watchfaceInfo.fileName.equals(name)) {
+ version = watchfaceInfo.version;
+ break;
+ }
+ }
+ String filename = name + "_" + version;
+ return filename;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfaceParams.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfaceParams.java
new file mode 100644
index 000000000..e29476dd1
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfaceParams.java
@@ -0,0 +1,57 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
+
+public class GetWatchfaceParams extends Request{
+
+ private static final Logger LOG = LoggerFactory.getLogger(GetWatchfaceParams.class);
+
+ public GetWatchfaceParams(HuaweiSupportProvider support) {
+ super(support);
+ this.serviceId = Watchface.id;
+ this.commandId = Watchface.WatchfaceParams.id;
+
+ }
+
+ @Override
+ protected List createRequest() throws RequestCreationException {
+ try {
+ return new Watchface.WatchfaceParams.Request(paramsProvider).serialize();
+ } catch (HuaweiPacket.CryptoException e) {
+ throw new RequestCreationException(e);
+ }
+ }
+
+ @Override
+ protected void processResponse() throws ResponseParseException {
+ if (!(receivedPacket instanceof Watchface.WatchfaceParams.Response))
+ throw new ResponseTypeMismatchException(receivedPacket, Watchface.WatchfaceParams.Response.class);
+
+ Watchface.WatchfaceParams.Response resp = (Watchface.WatchfaceParams.Response)(receivedPacket);
+ supportProvider.getHuaweiCoordinator().setWatchfaceDeviceParams(resp.params);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfacesList.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfacesList.java
new file mode 100644
index 000000000..644508d92
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfacesList.java
@@ -0,0 +1,57 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
+
+public class GetWatchfacesList extends Request{
+
+ private static final Logger LOG = LoggerFactory.getLogger(GetWatchfacesList.class);
+
+ public GetWatchfacesList(HuaweiSupportProvider support) {
+ super(support);
+ this.serviceId = Watchface.id;
+ this.commandId = Watchface.DeviceWatchInfo.id;
+
+ }
+
+ @Override
+ protected List createRequest() throws RequestCreationException {
+ try {
+ return new Watchface.DeviceWatchInfo.Request(paramsProvider).serialize();
+ } catch (HuaweiPacket.CryptoException e) {
+ throw new RequestCreationException(e);
+ }
+ }
+
+ @Override
+ protected void processResponse() throws ResponseParseException {
+ if (!(receivedPacket instanceof Watchface.DeviceWatchInfo.Response))
+ throw new ResponseTypeMismatchException(receivedPacket, Watchface.DeviceWatchInfo.Response.class);
+
+ Watchface.DeviceWatchInfo.Response resp = (Watchface.DeviceWatchInfo.Response)(receivedPacket);
+ supportProvider.getHuaweiWatchfaceManager().setInstalledWatchfaceInfoList(resp.watchfaceInfoList);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfacesNames.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfacesNames.java
new file mode 100644
index 000000000..dd9cc0e67
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfacesNames.java
@@ -0,0 +1,60 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
+
+public class GetWatchfacesNames extends Request{
+
+ private static final Logger LOG = LoggerFactory.getLogger(GetWatchfacesNames.class);
+
+ List watchfaceInfoList;
+ public GetWatchfacesNames(HuaweiSupportProvider support, List list) {
+ super(support);
+ this.serviceId = Watchface.id;
+ this.commandId = Watchface.WatchfaceNameInfo.id;
+ this.watchfaceInfoList = list;
+
+ }
+
+ @Override
+ protected List createRequest() throws RequestCreationException {
+ try {
+ return new Watchface.WatchfaceNameInfo.Request(paramsProvider, this.watchfaceInfoList).serialize();
+ } catch (HuaweiPacket.CryptoException e) {
+ throw new RequestCreationException(e);
+ }
+ }
+
+ @Override
+ protected void processResponse() throws ResponseParseException {
+ if (!(receivedPacket instanceof Watchface.WatchfaceNameInfo.Response))
+ throw new ResponseTypeMismatchException(receivedPacket, Watchface.WatchfaceNameInfo.Response.class);
+
+ Watchface.WatchfaceNameInfo.Response resp = (Watchface.WatchfaceNameInfo.Response)(receivedPacket);
+ supportProvider.getHuaweiWatchfaceManager().setWatchfacesNames(resp.watchFaceNames);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadAck.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadAck.java
new file mode 100644
index 000000000..bb48ebdf8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadAck.java
@@ -0,0 +1,46 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
+
+public class SendFileUploadAck extends Request {
+ byte noEncryption = 0;
+ byte fileType = 1;
+ public SendFileUploadAck(HuaweiSupportProvider support, byte noEncryption, byte fileType) {
+ super(support);
+ this.serviceId = FileUpload.id;
+ this.commandId = FileUpload.FileUploadConsultAck.id;
+ this.noEncryption = noEncryption;
+ this.fileType = fileType;
+ this.addToResponse = false;
+ }
+
+ @Override
+ protected List createRequest() throws RequestCreationException {
+ try {
+ return new FileUpload.FileUploadConsultAck.Request(this.paramsProvider, this.noEncryption, this.fileType).serialize();
+ } catch (HuaweiPacket.CryptoException e) {
+ throw new RequestCreationException(e);
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadChunk.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadChunk.java
new file mode 100644
index 000000000..d61cfd3d9
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadChunk.java
@@ -0,0 +1,45 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiUploadManager;
+
+public class SendFileUploadChunk extends Request {
+ HuaweiUploadManager huaweiUploadManager;
+ public SendFileUploadChunk(HuaweiSupportProvider support,
+ HuaweiUploadManager watchfaceManager) {
+ super(support);
+ this.huaweiUploadManager = watchfaceManager;
+ this.serviceId = FileUpload.id;
+ this.commandId = FileUpload.FileNextChunkSend.id;
+ this.addToResponse = false;
+ }
+
+ @Override
+ protected List createRequest() throws RequestCreationException {
+ return new FileUpload.FileNextChunkSend(this.paramsProvider).serializeFileChunk(
+ huaweiUploadManager.getCurrentChunk(),
+ huaweiUploadManager.getCurrentUploadPosition(),
+ huaweiUploadManager.getUnitSize()
+ );
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadComplete.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadComplete.java
new file mode 100644
index 000000000..80a4770d6
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadComplete.java
@@ -0,0 +1,47 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
+
+public class SendFileUploadComplete extends Request {
+
+ byte fileType = 0;
+ public SendFileUploadComplete(HuaweiSupportProvider support, byte fileType) {
+ super(support);
+
+ this.serviceId = FileUpload.id;
+ this.commandId = FileUpload.FileUploadResult.id;
+ this.fileType = fileType;
+ this.addToResponse = false;
+ }
+
+
+ @Override
+ protected List createRequest() throws RequestCreationException {
+ try {
+ return new FileUpload.FileUploadResult.Request(this.paramsProvider, this.fileType).serialize();
+ } catch (HuaweiPacket.CryptoException e) {
+ throw new RequestCreationException(e);
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadHash.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadHash.java
new file mode 100644
index 000000000..c86de3f33
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadHash.java
@@ -0,0 +1,50 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiUploadManager;
+
+public class SendFileUploadHash extends Request{
+ HuaweiUploadManager huaweiUploadManager;
+ public SendFileUploadHash(HuaweiSupportProvider support,
+ HuaweiUploadManager huaweiUploadManager) {
+ super(support);
+ this.huaweiUploadManager = huaweiUploadManager;
+ this.serviceId = FileUpload.id;
+ this.commandId = FileUpload.FileHashSend.id;
+ this.addToResponse = false;
+ }
+
+
+ @Override
+ protected List createRequest() throws RequestCreationException {
+ try {
+ return new FileUpload.FileHashSend.Request(this.paramsProvider,
+ huaweiUploadManager.getFileSHA256(),
+ huaweiUploadManager.getFileType()
+ ).serialize();
+ } catch (HuaweiPacket.CryptoException e) {
+ throw new RequestCreationException(e);
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadInfo.java
new file mode 100644
index 000000000..c8c38ecff
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadInfo.java
@@ -0,0 +1,52 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiUploadManager;
+
+public class SendFileUploadInfo extends Request{
+ HuaweiUploadManager huaweiUploadManager;
+ public SendFileUploadInfo(HuaweiSupportProvider support,
+ HuaweiUploadManager huaweiUploadManager) {
+ super(support);
+ this.huaweiUploadManager = huaweiUploadManager;
+ this.serviceId = FileUpload.id;
+ this.commandId = FileUpload.FileInfoSend.id;
+ this.addToResponse = false;
+
+ }
+
+ @Override
+ protected List createRequest() throws RequestCreationException {
+ try {
+ return new FileUpload.FileInfoSend.Request(this.paramsProvider,
+ huaweiUploadManager.getFileSize(),
+ huaweiUploadManager.getFileName(),
+ huaweiUploadManager.getFileType()
+ ).serialize();
+ } catch (HuaweiPacket.CryptoException e) {
+ throw new RequestCreationException(e);
+ }
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendWatchfaceConfirm.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendWatchfaceConfirm.java
new file mode 100644
index 000000000..8d8b08f72
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendWatchfaceConfirm.java
@@ -0,0 +1,46 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
+
+public class SendWatchfaceConfirm extends Request {
+ private String fileName;
+ public SendWatchfaceConfirm(HuaweiSupportProvider support, String filename) {
+ super(support);
+ this.serviceId = Watchface.id;
+ this.commandId = Watchface.WatchfaceConfirm.id;
+ this.fileName = filename;
+ this.addToResponse = false;
+ }
+
+ @Override
+ protected List createRequest() throws RequestCreationException {
+ try {
+ return new Watchface.WatchfaceConfirm.Request(this.paramsProvider, this.fileName ).serialize();
+ } catch (HuaweiPacket.CryptoException e) {
+ throw new RequestCreationException(e);
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendWatchfaceOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendWatchfaceOperation.java
new file mode 100644
index 000000000..7fd9ff595
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendWatchfaceOperation.java
@@ -0,0 +1,46 @@
+/* Copyright (C) 2024 Vitalii Tomin
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+
+package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
+import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
+
+public class SendWatchfaceOperation extends Request {
+ private String fileName;
+ private byte operation;
+ public SendWatchfaceOperation(HuaweiSupportProvider support, String filename, byte operation) {
+ super(support);
+ this.serviceId = Watchface.id;
+ this.commandId = Watchface.WatchfaceOperation.id;
+ this.fileName = filename;
+ this.operation = operation;
+ }
+
+ @Override
+ protected List createRequest() throws RequestCreationException {
+ try {
+ return new Watchface.WatchfaceOperation.Request(this.paramsProvider, this.fileName, this.operation ).serialize();
+ } catch (HuaweiPacket.CryptoException e) {
+ throw new RequestCreationException(e);
+ }
+ }
+
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3752981ff..9a2717a8c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2815,4 +2815,5 @@
SPO2
Huawei Account
Huawei account used in pairing process. Setting it allows to pair without factory reset.
+ Watchface resolution doesnt match device screen. Watchface is %1$s device screen is %2$s