mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-31 21:15:50 +01:00
initial commit
This commit is contained in:
parent
afa6cb3a96
commit
a256decfd0
@ -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()) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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<byte[]> 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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<NotificationHRConfiguration> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user