1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-31 21:15:50 +01:00

initial commit

This commit is contained in:
dakhnod 2019-12-15 14:58:19 +01:00
parent afa6cb3a96
commit a256decfd0
21 changed files with 996 additions and 35 deletions

View File

@ -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()) {

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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();
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}