From a256decfd0d11a9e55b6f362f58031396284fd42 Mon Sep 17 00:00:00 2001 From: dakhnod Date: Sun, 15 Dec 2019 14:58:19 +0100 Subject: [PATCH] initial commit --- .../devices/qhybrid/ConfigActivity.java | 6 +- .../qhybrid/NotificationHRConfiguration.java | 25 ++ .../devices/qhybrid/QHybridSupport.java | 7 + .../qhybrid/adapter/WatchAdapterFactory.java | 5 + .../adapter/fossil/FossilWatchAdapter.java | 3 +- .../fossil_hr/FossilHRWatchAdapter.java | 96 ++++++++ .../NotificationFilterPutRequest.java | 11 +- .../notification/PlayNotificationRequest.java | 40 +--- .../VerifyPrivateKeyRequest.java | 109 +++++++++ .../fossil_hr/file/AssetFilePutRequest.java | 43 ++++ .../fossil_hr/file/FilePutRawRequest.java | 214 ++++++++++++++++++ .../requests/fossil_hr/image/Image.java | 40 ++++ .../fossil_hr/image/ImagesPutRequest.java | 34 +++ .../fossil_hr/json/JsonPutRequest.java | 13 ++ .../NotificationFilterPutHRRequest.java | 100 ++++++++ .../notification/NotificationImage.java | 19 ++ .../NotificationImagePutRequest.java | 54 +++++ .../PlayNotificationHRRequest.java | 87 +++++++ .../requests/fossil_hr/widget/Widget.java | 60 +++++ .../fossil_hr/widget/WidgetsPutRequest.java | 36 +++ .../devices/qhybrid/utils/StringUtils.java | 29 +++ 21 files changed, 996 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/NotificationHRConfiguration.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/authentication/VerifyPrivateKeyRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/AssetFilePutRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FilePutRawRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/Image.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/ImagesPutRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/json/JsonPutRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationFilterPutHRRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationImage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationImagePutRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/PlayNotificationHRRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/widget/Widget.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/widget/WidgetsPutRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/utils/StringUtils.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/ConfigActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/ConfigActivity.java index a6625f13a..7734133a8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/ConfigActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/ConfigActivity.java @@ -414,7 +414,11 @@ public class ConfigActivity extends AbstractGBActivity { }); } - final String buttonJson = device.getDeviceInfo(FossilWatchAdapter.ITEM_BUTTONS).getDetails(); + ItemWithDetails item = device.getDeviceInfo(FossilWatchAdapter.ITEM_BUTTONS); + String buttonJson = null; + if(item != null) { + buttonJson = item.getDetails(); + } try { JSONArray buttonConfig_; if (buttonJson == null || buttonJson.isEmpty()) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/NotificationHRConfiguration.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/NotificationHRConfiguration.java new file mode 100644 index 000000000..ad62c548f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/NotificationHRConfiguration.java @@ -0,0 +1,25 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid; + +import android.util.Log; + +import java.io.Serializable; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest; + +public class NotificationHRConfiguration implements Serializable { + private String packageName; + private long id = -1; + + public NotificationHRConfiguration(String packageName, long id) { + this.packageName = packageName; + this.id = id; + } + + public String getPackageName() { + return packageName; + } + + public long getId() { + return id; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java index 27fc36c8a..6c32b6a70 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java @@ -49,6 +49,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration; @@ -68,6 +69,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateA import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapter; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapterFactory; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.DownloadFileRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -310,6 +312,11 @@ public class QHybridSupport extends QHybridBaseSupport { public void onNotification(NotificationSpec notificationSpec) { log("notif from " + notificationSpec.sourceAppId + " " + notificationSpec.sender + " " + notificationSpec.phoneNumber); //new Exception().printStackTrace(); + + if(this.watchAdapter instanceof FossilHRWatchAdapter){ + if(((FossilHRWatchAdapter) watchAdapter).playRawNotification(notificationSpec)) return; + } + String packageName = notificationSpec.sourceName; NotificationConfiguration config = null; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/WatchAdapterFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/WatchAdapterFactory.java index 6a29d90ae..8f1254ed4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/WatchAdapterFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/WatchAdapterFactory.java @@ -18,12 +18,17 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.misfit.MisfitWatchAdapter; public final class WatchAdapterFactory { public final WatchAdapter createWatchAdapter(String firmwareVersion, QHybridSupport deviceSupport){ + char hardwareVersion = firmwareVersion.charAt(2); + if(hardwareVersion == '1') return new FossilHRWatchAdapter(deviceSupport); + char major = firmwareVersion.charAt(6); switch (major){ + case '0': return new MisfitWatchAdapter(deviceSupport); case '1': return new MisfitWatchAdapter(deviceSupport); case '2': return new FossilWatchAdapter(deviceSupport); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil/FossilWatchAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil/FossilWatchAdapter.java index 679914c70..1f4efe435 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil/FossilWatchAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil/FossilWatchAdapter.java @@ -443,6 +443,7 @@ public class FossilWatchAdapter extends WatchAdapter { } case "3dda0002-957f-7d4a-34a6-74696673696d": case "3dda0004-957f-7d4a-34a6-74696673696d": + case "3dda0005-957f-7d4a-34a6-74696673696d": case "3dda0003-957f-7d4a-34a6-74696673696d": { if (fossilRequest != null) { boolean requestFinished; @@ -591,7 +592,7 @@ public class FossilWatchAdapter extends WatchAdapter { queueNextRequest(); } - void queueWrite(Request request) { + protected void queueWrite(Request request) { if (request instanceof SetDeviceStateRequest) queueWrite((SetDeviceStateRequest) request, false); else if (request instanceof RequestMtuRequest) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java new file mode 100644 index 000000000..4925fa241 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java @@ -0,0 +1,96 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr; + +import android.os.Build; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.zip.CRC32; + +import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration; +import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.RequestMtuRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.SetDeviceStateRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.NotificationFilterPutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.VerifyPrivateKeyRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.Image; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.ImagesPutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationFilterPutHRRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationImagePutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.PlayNotificationHRRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetCurrentStepCountRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.utils.StringUtils; + +public class FossilHRWatchAdapter extends FossilWatchAdapter { + public FossilHRWatchAdapter(QHybridSupport deviceSupport) { + super(deviceSupport); + } + + @Override + public void initialize() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + queueWrite(new RequestMtuRequest(512)); + } + + queueWrite(new VerifyPrivateKeyRequest( + new byte[]{(byte) 0x60, (byte) 0x26, (byte) 0xB7, (byte) 0xFD, (byte) 0xB2, (byte) 0x6D, (byte) 0x05, (byte) 0x5E, (byte) 0xDA, (byte) 0xF7, (byte) 0x4B, (byte) 0x49, (byte) 0x98, (byte) 0x78, (byte) 0x02, (byte) 0x38}, + getDeviceSupport().getQueue() + )); + + try { + FileInputStream fis = new FileInputStream("/sdcard/Q/images/icWhatsapp.icon"); + byte[] whatsappData = new byte[fis.available()]; + fis.read(whatsappData); + fis.close(); + + fis = new FileInputStream("/sdcard/Q/images/icTwitter.icon"); + byte[] twitterData = new byte[fis.available()]; + fis.read(twitterData); + fis.close(); + + queueWrite(new NotificationImagePutRequest( + new String[]{ + "twitter", + "com.whatsapp", + }, + new byte[][]{ + twitterData, + whatsappData, + }, + this)); + } catch (IOException e) { + e.printStackTrace(); + } + + queueWrite(new NotificationFilterPutHRRequest(new NotificationHRConfiguration[]{ + new NotificationHRConfiguration("twitter", -1), + new NotificationHRConfiguration("com.whatsapp", -1), + }, this)); + + queueWrite(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayNotificationRequest("com.whatsapp", "Test App", "this is a generic message", this)); + queueWrite(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayNotificationRequest("twitter", "Twitter", "huehuehue", this)); + + // syncConfiguration(); + + queueWrite(new SetDeviceStateRequest(GBDevice.State.INITIALIZED)); + } + + @Override + public void setActivityHand(double progress) { + // super.setActivityHand(progress); + } + + public boolean playRawNotification(NotificationSpec notificationSpec) { + String sender = notificationSpec.sender; + if(sender == null) sender = notificationSpec.sourceName; + queueWrite(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayNotificationRequest("generic", notificationSpec.sourceName, notificationSpec.body, this)); + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/NotificationFilterPutRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/NotificationFilterPutRequest.java index 0514d4aad..291dc9e47 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/NotificationFilterPutRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/NotificationFilterPutRequest.java @@ -74,11 +74,12 @@ public class NotificationFilterPutRequest extends FilePutRequest { } enum PacketID{ - PACKAGE_NAME((byte) 1), - SENDER_NAME((byte) 2), - PACKAGE_NAME_CRC((byte) 4), - GROUP_ID((byte) 128), - APP_DISPLAY_NAME((byte) 129), + PACKAGE_NAME((byte) 0x01), + SENDER_NAME((byte) 0x02), + PACKAGE_NAME_CRC((byte) 0x04), + GROUP_ID((byte) 0x80), + APP_DISPLAY_NAME((byte) 0x81), + ICON((byte) 0x82), PRIORITY((byte) 0xC1), MOVEMENT((byte) 0xC2), VIBRATION((byte) 0xC3); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/PlayNotificationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/PlayNotificationRequest.java index b6f973594..c8e6f4c15 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/PlayNotificationRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/PlayNotificationRequest.java @@ -23,39 +23,39 @@ import java.util.zip.CRC32; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.utils.StringUtils; public class PlayNotificationRequest extends FilePutRequest { public PlayNotificationRequest(String packageName, FossilWatchAdapter adapter) { - // super((short) 0x0900, createFile("org.telegram.messenger", "org.telegram.messenger", "org.telegram.messenger"), adapter); - super((short) 0x0900, createFile(packageName), adapter); + super((short) 0x0900, createFile(packageName, packageName, packageName), adapter); } - private static byte[] createFile(String packageName){ + public PlayNotificationRequest(String packageName, String sender, String message, FossilWatchAdapter adapter) { + super((short) 0x0900, createFile(packageName, sender, message), adapter); + } + + + private static byte[] createFile(String packageName, String sender, String message){ CRC32 crc = new CRC32(); crc.update(packageName.getBytes()); - return createFile(packageName, packageName, packageName, (int)crc.getValue()); + return createFile(packageName, sender, message, (int)crc.getValue()); } private static byte[] createFile(String title, String sender, String message, int packageCrc) { - // return new byte[]{(byte) 0x57, (byte) 0x00, (byte) 0x0A, (byte) 0x03, (byte) 0x02, (byte) 0x04, (byte) 0x04, (byte) 0x17, (byte) 0x17, (byte) 0x17, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x49, (byte) 0x7B, (byte) 0x3B, (byte) 0x62, (byte) 0x6F, (byte) 0x72, (byte) 0x67, (byte) 0x2E, (byte) 0x74, (byte) 0x65, (byte) 0x6C, (byte) 0x65, (byte) 0x67, (byte) 0x72, (byte) 0x61, (byte) 0x6D, (byte) 0x2E, (byte) 0x6D, (byte) 0x65, (byte) 0x73, (byte) 0x73, (byte) 0x65, (byte) 0x6E, (byte) 0x67, (byte) 0x65, (byte) 0x72, (byte) 0x00, (byte) 0x6F, (byte) 0x72, (byte) 0x67, (byte) 0x2E, (byte) 0x74, (byte) 0x65, (byte) 0x6C, (byte) 0x65, (byte) 0x67, (byte) 0x72, (byte) 0x61, (byte) 0x6D, (byte) 0x2E, (byte) 0x6D, (byte) 0x65, (byte) 0x73, (byte) 0x73, (byte) 0x65, (byte) 0x6E, (byte) 0x67, (byte) 0x65, (byte) 0x72, (byte) 0x00, (byte) 0x6F, (byte) 0x72, (byte) 0x67, (byte) 0x2E, (byte) 0x74, (byte) 0x65, (byte) 0x6C, (byte) 0x65, (byte) 0x67, (byte) 0x72, (byte) 0x61, (byte) 0x6D, (byte) 0x2E, (byte) 0x6D, (byte) 0x65, (byte) 0x73, (byte) 0x73, (byte) 0x65, (byte) 0x6E, (byte) 0x67, (byte) 0x65, (byte) 0x72, (byte) 0x00}; - // gwb.k(var6, "ByteBuffer.allocate(10)"); byte lengthBufferLength = (byte) 10; byte typeId = 3; byte flags = getFlags(); byte uidLength = (byte) 4; byte appBundleCRCLength = (byte) 4; - String nullTerminatedTitle = terminateNull(title); + String nullTerminatedTitle = StringUtils.terminateNull(title); Charset charsetUTF8 = Charset.forName("UTF-8"); byte[] titleBytes = nullTerminatedTitle.getBytes(charsetUTF8); - // gwb.k(var13, "(this as java.lang.String).getBytes(charset)"); - String nullTerminatedSender = terminateNull(sender); + String nullTerminatedSender = StringUtils.terminateNull(sender); byte[] senderBytes = nullTerminatedSender.getBytes(charsetUTF8); - // gwb.k(var15, "(this as java.lang.String).getBytes(charset)"); - String nullTerminatedMessage = terminateNull(message); + String nullTerminatedMessage = StringUtils.terminateNull(message); byte[] messageBytes = nullTerminatedMessage.getBytes(charsetUTF8); - // gwb.k(var17, "(this as java.lang.String).getBytes(charset)"); short mainBufferLength = (short) (lengthBufferLength + uidLength + appBundleCRCLength + titleBytes.length + senderBytes.length + messageBytes.length); @@ -72,12 +72,10 @@ public class PlayNotificationRequest extends FilePutRequest { lengthBuffer.put((byte) messageBytes.length); ByteBuffer mainBuffer = ByteBuffer.allocate(mainBufferLength); - // gwb.k(var11, "ByteBuffer.allocate(totalLen.toInt())"); mainBuffer.order(ByteOrder.LITTLE_ENDIAN); mainBuffer.put(lengthBuffer.array()); lengthBuffer = ByteBuffer.allocate(mainBufferLength - lengthBufferLength); - // gwb.k(var6, "ByteBuffer.allocate(totalLen - headerLen)"); lengthBuffer.order(ByteOrder.LITTLE_ENDIAN); lengthBuffer.putInt(0); lengthBuffer.putInt(packageCrc); @@ -92,18 +90,4 @@ public class PlayNotificationRequest extends FilePutRequest { return (byte) 2; } - public static String terminateNull(String input){ - if(input.length() == 0){ - return new String(new byte[]{(byte) 0}); - } - char lastChar = input.charAt(input.length() - 1); - if(lastChar == 0) return input; - - byte[] newArray = new byte[input.length() + 1]; - System.arraycopy(input.getBytes(), 0, newArray, 0, input.length()); - - newArray[newArray.length - 1] = 0; - - return new String(newArray); - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/authentication/VerifyPrivateKeyRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/authentication/VerifyPrivateKeyRequest.java new file mode 100644 index 000000000..9c0f8a51e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/authentication/VerifyPrivateKeyRequest.java @@ -0,0 +1,109 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication; + +import android.bluetooth.BluetoothGattCharacteristic; + +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Random; +import java.util.UUID; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest; + +public class VerifyPrivateKeyRequest extends FossilRequest { + private final BtLEQueue queue; + private byte[] key; + private boolean isFinished = false; + + public VerifyPrivateKeyRequest(byte[] key, BtLEQueue queue) { + this.queue = queue; + this.key = key; + } + + @Override + public void handleResponse(BluetoothGattCharacteristic characteristic) { + super.handleResponse(characteristic); + byte[] value = characteristic.getValue(); + + ByteBuffer buffer = ByteBuffer.wrap(value); + + if (value[1] == 1) { + try { + byte[] bytesToDecrypt = new byte[16]; + + buffer.position(4); + + buffer.get(bytesToDecrypt, 0, 16); + + SecretKeySpec keySpec = new SecretKeySpec(this.key, "AES"); + Cipher cipher = null; + cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); + byte[] result = cipher.doFinal(bytesToDecrypt); + + byte[] bytesToEncrypt = new byte[16]; + + System.arraycopy(result, 0, bytesToEncrypt, 8, 8); + System.arraycopy(result, 8, bytesToEncrypt, 0, 8); + + cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); + result = cipher.doFinal(bytesToEncrypt); + + byte[] payload = new byte[19]; + payload[0] = 2; + payload[1] = 2; + payload[2] = 1; + + System.arraycopy(result, 0, payload, 3, 16); + + new TransactionBuilder("send encrypted random numbers") + .write(characteristic, payload) + .queue(this.queue); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } else if (value[1] == 2) { + if (value[2] != 0) throw new RuntimeException("Authentication error: " + value[2]); + + this.isFinished = true; + } + } + + @Override + public boolean isFinished() { + return isFinished; + } + + @Override + public byte[] getStartSequence() { + ByteBuffer buffer = ByteBuffer.allocate(11); + + buffer.put((byte) 0x02); + buffer.put((byte) 0x01); + buffer.put((byte) 0x01); + + byte[] random = new byte[8]; + + new Random().nextBytes(random); + + buffer.put(random); + + return buffer.array(); + } + + @Override + public UUID getRequestUUID() { + return UUID.fromString("3dda0005-957f-7d4a-34a6-74696673696d"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/AssetFilePutRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/AssetFilePutRequest.java new file mode 100644 index 000000000..6f0214376 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/AssetFilePutRequest.java @@ -0,0 +1,43 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.utils.StringUtils; + +public class AssetFilePutRequest extends FilePutRequest { + public AssetFilePutRequest(byte[] fileName, byte[] file, FossilWatchAdapter adapter) { + super((short) 0x0701, prepareFileData(fileName, file), adapter); + } + public AssetFilePutRequest(byte[][] fileNames, byte[][] files, FossilWatchAdapter adapter) throws IOException { + super((short) 0x0701, prepareFileData(fileNames, files), adapter); + } + + private static byte[] prepareFileData(byte[][] fileNames, byte[][] files) throws IOException { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + for(int i = 0; i < fileNames.length; i++){ + stream.write( + prepareFileData(fileNames[i], files[i]) + ); + } + + return stream.toByteArray(); + } + + private static byte[] prepareFileData(byte[] fileNameNullTerminated, byte[] file){ + ByteBuffer buffer = ByteBuffer.allocate(fileNameNullTerminated.length + 2 + file.length); + + buffer.order(ByteOrder.LITTLE_ENDIAN); + + buffer.putShort((short)(fileNameNullTerminated.length + file.length)); + buffer.put(fileNameNullTerminated); + buffer.put(file); + + return buffer.array(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FilePutRawRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FilePutRawRequest.java new file mode 100644 index 000000000..abdf8ad91 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FilePutRawRequest.java @@ -0,0 +1,214 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.UUID; +import java.util.zip.CRC32; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class FilePutRawRequest extends FossilRequest { + public enum UploadState {INITIALIZED, UPLOADING, CLOSING, UPLOADED} + + public UploadState state; + + public ArrayList packets = new ArrayList<>(); + + private short handle; + + private FossilWatchAdapter adapter; + + byte[] file; + + int fullCRC; + + public FilePutRawRequest(short handle, byte[] file, FossilWatchAdapter adapter) { + this.handle = handle; + this.adapter = adapter; + + int fileLength = file.length; + ByteBuffer buffer = this.createBuffer(); + buffer.putShort(1, handle); + buffer.putInt(3, 0); + buffer.putInt(7, fileLength); + buffer.putInt(11, fileLength); + + this.data = buffer.array(); + + this.file = file; + + state = UploadState.INITIALIZED; + } + + public short getHandle() { + return handle; + } + + @Override + public void handleResponse(BluetoothGattCharacteristic characteristic) { + byte[] value = characteristic.getValue(); + if (characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")) { + int responseType = value[0] & 0x0F; + log("response: " + responseType); + switch (responseType) { + case 3: { + if (value.length != 5 || (value[0] & 0x0F) != 3) { + throw new RuntimeException("wrong answer header"); + } + state = UploadState.UPLOADING; + + TransactionBuilder transactionBuilder = new TransactionBuilder("file upload"); + BluetoothGattCharacteristic uploadCharacteristic = adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0004-957f-7d4a-34a6-74696673696d")); + + this.prepareFilePackets(this.file); + + for (byte[] packet : packets) { + transactionBuilder.write(uploadCharacteristic, packet); + } + + transactionBuilder.queue(adapter.getDeviceSupport().getQueue()); + break; + } + case 8: { + if (value.length == 4) return; + ByteBuffer buffer = ByteBuffer.wrap(value); + buffer.order(ByteOrder.LITTLE_ENDIAN); + short handle = buffer.getShort(1); + int crc = buffer.getInt(8); + byte status = value[3]; + + if (status != 0) { + throw new RuntimeException("upload status: " + status); + } + + if (handle != this.handle) { + throw new RuntimeException("wrong response handle"); + } + + if (crc != this.fullCRC) { + throw new RuntimeException("file upload exception: wrong crc"); + } + + + ByteBuffer buffer2 = ByteBuffer.allocate(3); + buffer2.order(ByteOrder.LITTLE_ENDIAN); + buffer2.put((byte) 4); + buffer2.putShort(this.handle); + + new TransactionBuilder("file close") + .write( + adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d")), + buffer2.array() + ) + .queue(adapter.getDeviceSupport().getQueue()); + + this.state = UploadState.CLOSING; + break; + } + case 4: { + if (value.length == 9) return; + if (value.length != 4 || (value[0] & 0x0F) != 4) { + throw new RuntimeException("wrong file closing header"); + } + ByteBuffer buffer = ByteBuffer.wrap(value); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + short handle = buffer.getShort(1); + + if (handle != this.handle) { + onFilePut(false); + throw new RuntimeException("wrong file closing handle"); + } + + byte status = buffer.get(3); + + if (status != 0) { + onFilePut(false); + throw new RuntimeException("wrong closing status: " + status); + } + + this.state = UploadState.UPLOADED; + + onFilePut(true); + + log("uploaded file"); + + break; + } + case 9: { + this.onFilePut(false); + throw new RuntimeException("file put timeout"); + /*timeout = true; + ByteBuffer buffer2 = ByteBuffer.allocate(3); + buffer2.order(ByteOrder.LITTLE_ENDIAN); + buffer2.put((byte) 4); + buffer2.putShort(this.handle); + + new TransactionBuilder("file close") + .write( + adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d")), + buffer2.array() + ) + .queue(adapter.getDeviceSupport().getQueue()); + + this.state = UploadState.CLOSING; + break;*/ + } + } + } + } + + @Override + public boolean isFinished() { + return this.state == UploadState.UPLOADED; + } + + private void prepareFilePackets(byte[] file) { + int maxPacketSize = adapter.getMTU() - 4; + + byte[] data = file; + + CRC32 fullCRC = new CRC32(); + + fullCRC.update(data); + this.fullCRC = (int) fullCRC.getValue(); + + int packetCount = (int) Math.ceil(data.length / (float) maxPacketSize); + + for (int i = 0; i < packetCount; i++) { + int currentPacketLength = Math.min(maxPacketSize, data.length - i * maxPacketSize); + byte[] packet = new byte[currentPacketLength + 1]; + packet[0] = (byte) i; + System.arraycopy(data, i * maxPacketSize, packet, 1, currentPacketLength); + + packets.add(packet); + } + } + + public void onFilePut(boolean success) { + } + + @Override + public byte[] getStartSequence() { + return new byte[]{0x03}; + } + + @Override + public int getPayloadLength() { + return 15; + } + + @Override + public UUID getRequestUUID() { + return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/Image.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/Image.java new file mode 100644 index 000000000..0429874e5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/Image.java @@ -0,0 +1,40 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +public class Image { + private int angle, distance, indexZ; + private String imageFile; + + public Image(int angle, int distance, int indexZ, String imageFile) { + this.angle = angle; + this.distance = distance; + this.indexZ = indexZ; + this.imageFile = imageFile; + } + + @NonNull + @Override + public String toString() { + return toJsonObject().toString(); + } + + public JSONObject toJsonObject(){ + try { + return new JSONObject() + .put("image_name", this.imageFile) + .put("pos", + new JSONObject() + .put("angle", angle) + .put("distance", distance) + .put("z_index", indexZ) + ); + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/ImagesPutRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/ImagesPutRequest.java new file mode 100644 index 000000000..bf7cdbff2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/ImagesPutRequest.java @@ -0,0 +1,34 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.json.JsonPutRequest; + +public class ImagesPutRequest extends JsonPutRequest { + public ImagesPutRequest(Image[] images, FossilWatchAdapter adapter) { + super((short) 0x0501, prepareObject(images), adapter); + } + + private static JSONObject prepareObject(Image[] images){ + try { + JSONArray imageArray = new JSONArray(); + for (Image image : images) imageArray.put(image.toJsonObject()); + return new JSONObject() + .put("push", + new JSONObject() + .put("set", + new JSONObject() + .put("watchFace._.config.backgrounds", + imageArray + ) + ) + ); + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/json/JsonPutRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/json/JsonPutRequest.java new file mode 100644 index 000000000..5359355cf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/json/JsonPutRequest.java @@ -0,0 +1,13 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.json; + +import org.json.JSONObject; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FilePutRawRequest; + +public class JsonPutRequest extends FilePutRawRequest { + public JsonPutRequest(short handle, JSONObject object, FossilWatchAdapter adapter) { + super(handle, object.toString().getBytes(), adapter); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationFilterPutHRRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationFilterPutHRRequest.java new file mode 100644 index 000000000..43ee03b9f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationFilterPutHRRequest.java @@ -0,0 +1,100 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.zip.CRC32; + +import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration; +import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.utils.StringUtils; + +public class NotificationFilterPutHRRequest extends FilePutRequest { + public NotificationFilterPutHRRequest(NotificationHRConfiguration[] configs, FossilWatchAdapter adapter) { + super((short) 0x0C00, createFile(configs), adapter); + } + + + public NotificationFilterPutHRRequest(ArrayList configs, FossilWatchAdapter adapter) { + super((short) 0x0C00, createFile(configs.toArray(new NotificationHRConfiguration[0])), adapter); + } + + private static byte[] createFile(NotificationHRConfiguration[] configs) { + ByteBuffer buffer = ByteBuffer.allocate(configs.length * 28); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + for (NotificationHRConfiguration config : configs) { + buffer.putShort((short) 28); //packet length + + CRC32 crc = new CRC32(); + crc.update(config.getPackageName().getBytes()); + + byte[] crcBytes = ByteBuffer + .allocate(4) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt((int) crc.getValue()) + .array(); + + // 6 bytes + buffer.put(PacketID.PACKAGE_NAME_CRC.id) + .put((byte) 4) + .put(crcBytes); + + // 3 bytes + buffer.put(PacketID.GROUP_ID.id) + .put((byte) 1) + .put((byte) 2); + + // 3 bytes + buffer.put(PacketID.PRIORITY.id) + .put((byte) 1) + .put((byte) 0xFF); + + // 14 bytes + buffer.put(PacketID.ICON.id) + .put((byte) 0x0C) + .put((byte) 0xFF) + .put((byte) 0x00) + .put((byte) 0x09) + .put(StringUtils.bytesToHex(crcBytes).getBytes()) + .put((byte) 0x00); + + } + + return buffer.array(); + } + + enum PacketID { + PACKAGE_NAME((byte) 0x01), + SENDER_NAME((byte) 0x02), + PACKAGE_NAME_CRC((byte) 0x04), + GROUP_ID((byte) 0x80), + APP_DISPLAY_NAME((byte) 0x81), + ICON((byte) 0x82), + PRIORITY((byte) 0xC1), + MOVEMENT((byte) 0xC2), + VIBRATION((byte) 0xC3); + + byte id; + + PacketID(byte id) { + this.id = id; + } + } + + enum VibrationType { + SINGLE_SHORT((byte) 5), + DOUBLE_SHORT((byte) 6), + TRIPLE_SHORT((byte) 7), + SINGLE_LONG((byte) 8), + SILENT((byte) 9); + + byte id; + + VibrationType(byte id) { + this.id = id; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationImage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationImage.java new file mode 100644 index 000000000..a6573a98e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationImage.java @@ -0,0 +1,19 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification; + +public class NotificationImage { + private String packageName; + private byte[] imageData; + + public NotificationImage(String packageName, byte[] imageData) { + this.packageName = packageName; + this.imageData = imageData; + } + + public String getPackageName() { + return packageName; + } + + public byte[] getImageData() { + return imageData; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationImagePutRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationImagePutRequest.java new file mode 100644 index 000000000..baa7aa7a9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/NotificationImagePutRequest.java @@ -0,0 +1,54 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.stream.Stream; +import java.util.zip.CRC32; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.AssetFilePutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.utils.StringUtils; + +public class NotificationImagePutRequest extends AssetFilePutRequest { + private NotificationImagePutRequest(String packageName, byte[] file, FossilWatchAdapter adapter) { + super(prepareFileCrc(packageName), file, adapter); + } + + private NotificationImagePutRequest(NotificationImage image, FossilWatchAdapter adapter) { + super(prepareFileCrc(image.getPackageName()), image.getImageData(), adapter); + } + + public NotificationImagePutRequest(String[] fileNames, byte[][] files, FossilWatchAdapter adapter) throws IOException { + super(prepareFileCrc(fileNames), files, adapter); + } + + + private static byte[][] prepareFileCrc(String[] packageNames){ + byte[][] names = new byte[packageNames.length][]; + for (int i = 0; i < packageNames.length; i++){ + names[i] = prepareFileCrc(packageNames[i]); + } + return names; + } + + private static byte[] prepareFileCrc(String packageName){ + CRC32 crc = new CRC32(); + crc.update(packageName.getBytes()); + + String crcString = StringUtils.bytesToHex( + ByteBuffer + .allocate(4) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt((int) crc.getValue()) + .array() + ); + + ByteBuffer buffer = ByteBuffer.allocate(crcString.length() + 1) + .put(crcString.getBytes()) + .put((byte) 0x00); + + return buffer.array(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/PlayNotificationHRRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/PlayNotificationHRRequest.java new file mode 100644 index 000000000..d04158982 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/notification/PlayNotificationHRRequest.java @@ -0,0 +1,87 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.util.zip.CRC32; + +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayNotificationRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.utils.StringUtils; + +public class PlayNotificationHRRequest extends FilePutRequest { + + public PlayNotificationHRRequest(NotificationSpec spec, FossilWatchAdapter adapter) { + this(spec.sourceAppId, spec.sender, spec.body, adapter); + } + + public PlayNotificationHRRequest(String packageName, String sender, String message, FossilWatchAdapter adapter){ + super((short) 0x0900, createFile(packageName, sender, message), adapter); + } + + private static byte[] createFile(String packageName, String sender, String message) { + byte lengthBufferLength = (byte) 10; + byte typeId = 3; + byte flags = getFlags(); + byte uidLength = (byte) 4; + byte appBundleCRCLength = (byte) 4; + String nullTerminatedTitle = StringUtils.terminateNull(packageName); + + Charset charsetUTF8 = Charset.forName("UTF-8"); + byte[] titleBytes = nullTerminatedTitle.getBytes(charsetUTF8); + String nullTerminatedSender = StringUtils.terminateNull(sender); + byte[] senderBytes = nullTerminatedSender.getBytes(charsetUTF8); + String nullTerminatedMessage = StringUtils.terminateNull(message); + byte[] messageBytes = nullTerminatedMessage.getBytes(charsetUTF8); + + short mainBufferLength = (short) (lengthBufferLength + uidLength + appBundleCRCLength + titleBytes.length + senderBytes.length + messageBytes.length); + + ByteBuffer lengthBuffer = ByteBuffer.allocate(lengthBufferLength); + lengthBuffer.order(ByteOrder.LITTLE_ENDIAN); + lengthBuffer.putShort(mainBufferLength); + lengthBuffer.put(lengthBufferLength); + lengthBuffer.put(typeId); + lengthBuffer.put(flags); + lengthBuffer.put(uidLength); + lengthBuffer.put(appBundleCRCLength); + lengthBuffer.put((byte) titleBytes.length); + lengthBuffer.put((byte) senderBytes.length); + lengthBuffer.put((byte) messageBytes.length); + + ByteBuffer mainBuffer = ByteBuffer.allocate(mainBufferLength); + mainBuffer.order(ByteOrder.LITTLE_ENDIAN); + mainBuffer.put(lengthBuffer.array()); + + lengthBuffer = ByteBuffer.allocate(mainBufferLength - lengthBufferLength); + lengthBuffer.order(ByteOrder.LITTLE_ENDIAN); + // lengthBuffer.putInt(0); + lengthBuffer.put((byte) 0x00); + lengthBuffer.put((byte) 0x00); + lengthBuffer.put((byte) 0x00); + lengthBuffer.put((byte) 0x00); + + CRC32 packageNameCrc = new CRC32(); + packageNameCrc.update(packageName.getBytes()); + // lengthBuffer.putInt((int) packageNameCrc.getValue()); + + lengthBuffer.putInt((int) 0); + + // lengthBuffer.put((byte) 0x19); + // lengthBuffer.put((byte) 0x38); + // lengthBuffer.put((byte) 0xE0); + // lengthBuffer.put((byte) 0xDA); + lengthBuffer.put(titleBytes); + lengthBuffer.put(senderBytes); + lengthBuffer.put(messageBytes); + mainBuffer.put(lengthBuffer.array()); + return mainBuffer.array(); + } + + + + private static byte getFlags(){ + return (byte) 2; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/widget/Widget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/widget/Widget.java new file mode 100644 index 000000000..b819e1637 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/widget/Widget.java @@ -0,0 +1,60 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class Widget { + private WidgetType widgetType; + int angle, distance; + + public Widget(WidgetType type, int angle, int distance){ + this.widgetType = type; + } + + @NonNull + @Override + public String toString() { + return toJson().toString(); + } + + public JSONObject toJson(){ + JSONObject object = new JSONObject(); + + try { + object + .put("name", widgetType.getIdentifier()) + .put("pos", + new JSONObject() + .put("angle", angle) + .put("distance", distance) + ) + .put("data", new JSONObject()) + .put("theme", + new JSONObject() + .put("font_color", "default") + ); + } catch (JSONException e) { + e.printStackTrace(); + } + + return object; + } + + + enum WidgetType{ + TIMEZONE("timeZone2SSE"); + + private String identifier; + + + WidgetType(String identifier){ + this.identifier = identifier; + } + + public String getIdentifier(){ + return this.identifier; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/widget/WidgetsPutRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/widget/WidgetsPutRequest.java new file mode 100644 index 000000000..5553cd8e4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/widget/WidgetsPutRequest.java @@ -0,0 +1,36 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FilePutRawRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.json.JsonPutRequest; + +public class WidgetsPutRequest extends JsonPutRequest { + public WidgetsPutRequest(Widget[] widgets, FossilWatchAdapter adapter) { + super((short) 0x0501, prepareFile(widgets), adapter); + } + + private static JSONObject prepareFile(Widget[] widgets){ + try { + JSONArray widgetArray = new JSONArray(widgets); + + JSONObject object = new JSONObject() + .put( + "push", + new JSONObject() + .put("set", + new JSONObject().put( + "watchFace._.config.comps", widgetArray + ) + ) + ); + return object; + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/utils/StringUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/utils/StringUtils.java new file mode 100644 index 000000000..4e9a94227 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/utils/StringUtils.java @@ -0,0 +1,29 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.utils; + +public class StringUtils extends nodomain.freeyourgadget.gadgetbridge.util.StringUtils { + public static String terminateNull(String input){ + if(input.length() == 0){ + return new String(new byte[]{(byte) 0}); + } + char lastChar = input.charAt(input.length() - 1); + if(lastChar == 0) return input; + + byte[] newArray = new byte[input.length() + 1]; + System.arraycopy(input.getBytes(), 0, newArray, 0, input.length()); + + newArray[newArray.length - 1] = 0; + + return new String(newArray); + } + + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = bytes.length - 1; j >= 0; j--) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } +}