From 4d0d9e298ec608d10bac4d467f6c1b0c713bca29 Mon Sep 17 00:00:00 2001 From: Vitaliy Tomin Date: Sat, 27 Apr 2024 21:37:15 +0000 Subject: [PATCH] huawei: feature: File upload and watchface management (#3671) Co-authored-by: Vitaliy Tomin Co-committed-by: Vitaliy Tomin --- .../devices/huawei/HuaweiBRCoordinator.java | 27 +- .../devices/huawei/HuaweiCoordinator.java | 51 +++ .../devices/huawei/HuaweiInstallHandler.java | 121 +++++++ .../devices/huawei/HuaweiLECoordinator.java | 28 +- .../devices/huawei/HuaweiPacket.java | 80 +++++ .../devices/huawei/packets/FileUpload.java | 203 ++++++++++++ .../devices/huawei/packets/Watchface.java | 247 ++++++++++++++ .../devices/huawei/AsynchronousResponse.java | 83 +++++ .../devices/huawei/HuaweiBRSupport.java | 25 ++ .../devices/huawei/HuaweiFwHelper.java | 135 ++++++++ .../devices/huawei/HuaweiLESupport.java | 27 ++ .../devices/huawei/HuaweiSupportProvider.java | 77 +++++ .../devices/huawei/HuaweiUploadManager.java | 149 +++++++++ .../huawei/HuaweiWatchfaceManager.java | 303 ++++++++++++++++++ .../huawei/requests/GetWatchfaceParams.java | 57 ++++ .../huawei/requests/GetWatchfacesList.java | 57 ++++ .../huawei/requests/GetWatchfacesNames.java | 60 ++++ .../huawei/requests/SendFileUploadAck.java | 46 +++ .../huawei/requests/SendFileUploadChunk.java | 45 +++ .../requests/SendFileUploadComplete.java | 47 +++ .../huawei/requests/SendFileUploadHash.java | 50 +++ .../huawei/requests/SendFileUploadInfo.java | 52 +++ .../huawei/requests/SendWatchfaceConfirm.java | 46 +++ .../requests/SendWatchfaceOperation.java | 46 +++ app/src/main/res/values/strings.xml | 1 + 25 files changed, 2057 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiInstallHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FileUpload.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Watchface.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFwHelper.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiUploadManager.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWatchfaceManager.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfaceParams.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfacesList.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWatchfacesNames.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadAck.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadChunk.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadComplete.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadHash.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendFileUploadInfo.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendWatchfaceConfirm.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendWatchfaceOperation.java 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 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 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 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