mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-01-23 08:07:33 +01:00
Add Fossil HR Activity Tracking (#1846)
This commit is contained in:
parent
f30be95209
commit
11d1fd08bd
@ -43,7 +43,7 @@ public class GBDaoGenerator {
|
||||
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Schema schema = new Schema(24, MAIN_PACKAGE + ".entities");
|
||||
Schema schema = new Schema(25, MAIN_PACKAGE + ".entities");
|
||||
|
||||
Entity userAttributes = addUserAttributes(schema);
|
||||
Entity user = addUserInfo(schema, userAttributes);
|
||||
@ -71,6 +71,7 @@ public class GBDaoGenerator {
|
||||
addZeTimeActivitySample(schema, user, device);
|
||||
addID115ActivitySample(schema, user, device);
|
||||
addJYouActivitySample(schema, user, device);
|
||||
addHybridHRActivitySample(schema, user, device);
|
||||
addCalendarSyncState(schema, device);
|
||||
addAlarms(schema, user, device);
|
||||
|
||||
@ -341,6 +342,23 @@ public class GBDaoGenerator {
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addHybridHRActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "HybridHRActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
|
||||
addCommonActivitySampleProperties("AbstractHybridHRActivitySample", activitySample, user, device);
|
||||
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty("calories").notNull();
|
||||
activitySample.addIntProperty("variability").notNull();
|
||||
activitySample.addIntProperty("max_variability").notNull();
|
||||
activitySample.addIntProperty("heartrate_quality").notNull();
|
||||
activitySample.addBooleanProperty("active").notNull();
|
||||
activitySample.addByteProperty("wear_type").notNull();
|
||||
addHeartRateProperties(activitySample);
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
|
||||
activitySample.setSuperclass(superClass);
|
||||
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");
|
||||
|
@ -0,0 +1,75 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.parser.ActivityEntry;
|
||||
|
||||
public class HybridHRActivitySampleProvider extends AbstractSampleProvider<HybridHRActivitySample> {
|
||||
public HybridHRActivitySampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDao<HybridHRActivitySample, ?> getSampleDao() {
|
||||
return getSession().getHybridHRActivitySampleDao();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Property getRawKindSampleProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return HybridHRActivitySampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return HybridHRActivitySampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int normalizeType(int rawType) {
|
||||
if(rawType == -1) return 0;
|
||||
return ActivityEntry.WEARING_STATE.fromValue((byte) rawType).getActivityKind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int toRawActivityKind(int activityKind) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float normalizeIntensity(int rawIntensity) {
|
||||
return rawIntensity / 63f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HybridHRActivitySample createActivitySample() {
|
||||
return new HybridHRActivitySample();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HybridHRActivitySample> getActivitySamples(int timestamp_from, int timestamp_to) {
|
||||
return super.getActivitySamples(timestamp_from, timestamp_to);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HybridHRActivitySample> getAllActivitySamples(int timestamp_from, int timestamp_to) {
|
||||
return super.getAllActivitySamples(timestamp_from, timestamp_to);
|
||||
}
|
||||
}
|
@ -85,7 +85,7 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -95,7 +95,7 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return null;
|
||||
return new HybridHRActivitySampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -132,7 +132,7 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
return this.isHybridHR();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,24 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.entities;
|
||||
|
||||
public abstract class AbstractHybridHRActivitySample extends AbstractActivitySample {
|
||||
abstract public int getCalories();
|
||||
abstract public byte getWear_type();
|
||||
|
||||
@Override
|
||||
public int getRawKind() {
|
||||
return getWear_type();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRawIntensity() {
|
||||
return getCalories();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserId(long userId) {}
|
||||
|
||||
@Override
|
||||
public long getUserId() {
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -34,11 +34,16 @@ import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HRConfigActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
@ -49,16 +54,23 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
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.parser.ActivityEntry;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.parser.ActivityFileParser;
|
||||
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.TimeConfigItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileDeleteRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayCallNotificationRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayTextNotificationRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.activity.ActivityFilesGetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.VerifyPrivateKeyRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.buttons.ButtonConfigurationPutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration.ConfigurationGetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration.ConfigurationPutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.AssetFilePutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FileEncryptedGetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FirmwareFilePutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.AssetImage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.AssetImageFactory;
|
||||
@ -74,6 +86,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fos
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomWidgetElement;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.Widget;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.WidgetsPutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ListFilesRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
@ -488,6 +501,64 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
||||
@Override
|
||||
public void onFetchActivityData() {
|
||||
syncSettings();
|
||||
|
||||
queueWrite(new VerifyPrivateKeyRequest(this.getSecretKey(), this));
|
||||
queueWrite(new FileLookupRequest((byte) 0x01, this){
|
||||
@Override
|
||||
public void handleFileLookup(final short fileHandle) {
|
||||
queueWrite(new FileEncryptedGetRequest(fileHandle, FossilHRWatchAdapter.this) {
|
||||
@Override
|
||||
public void handleFileData(byte[] fileData) {
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
ActivityFileParser parser = new ActivityFileParser();
|
||||
ArrayList<ActivityEntry> entries = parser.parseFile(fileData);
|
||||
HybridHRActivitySampleProvider provider = new HybridHRActivitySampleProvider(getDeviceSupport().getDevice(), dbHandler.getDaoSession());
|
||||
|
||||
HybridHRActivitySample[] samples = new HybridHRActivitySample[entries.size()];
|
||||
|
||||
for(int i = 0; i < entries.size(); i++){
|
||||
samples[i] = entries.get(i).toDAOActivitySample(DBHelper.getDevice(getDeviceSupport().getDevice(), dbHandler.getDaoSession()).getId());
|
||||
}
|
||||
|
||||
provider.addGBActivitySamples(samples);
|
||||
|
||||
writeFile(String.valueOf(System.currentTimeMillis()), fileData);
|
||||
queueWrite(new FileDeleteRequest(fileHandle));
|
||||
GB.toast("synced activity data", Toast.LENGTH_SHORT, GB.INFO);
|
||||
} catch (Exception ex) {
|
||||
GB.toast(getContext(), "Error saving steps data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
||||
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
|
||||
}
|
||||
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFileLookupError(FILE_LOOKUP_ERROR error) {
|
||||
if(error == FILE_LOOKUP_ERROR.FILE_EMPTY){
|
||||
GB.toast("activity file empty yet", Toast.LENGTH_LONG, GB.ERROR);
|
||||
}else{
|
||||
throw new RuntimeException("strange lookup stuff");
|
||||
}
|
||||
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void writeFile(String fileName, byte[] value){
|
||||
File activityDir = new File(getContext().getExternalFilesDir(null), "activity_hr");
|
||||
activityDir.mkdir();
|
||||
File f = new File(activityDir, fileName);
|
||||
try {
|
||||
f.createNewFile();
|
||||
FileOutputStream fos = new FileOutputStream(f);
|
||||
fos.write(value);
|
||||
fos.close();
|
||||
GB.toast("saved file data", Toast.LENGTH_SHORT, GB.INFO);
|
||||
} catch (IOException e) {
|
||||
GB.log("file error", GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void syncSettings() {
|
||||
@ -670,35 +741,28 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// this was used to enumerate the weather icons :)
|
||||
/*
|
||||
static int i = 0;
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
long ts = System.currentTimeMillis();
|
||||
ts /= 1000;
|
||||
try {
|
||||
JSONObject responseObject = new JSONObject()
|
||||
.put("res", new JSONObject()
|
||||
.put("id", 0) // seems the id does not matter?
|
||||
.put("set", new JSONObject()
|
||||
.put("weatherInfo", new JSONObject()
|
||||
.put("alive", ts + 60 * 60)
|
||||
.put("unit", "c")
|
||||
.put("temp", i)
|
||||
.put("cond_id", i++)
|
||||
)
|
||||
));
|
||||
|
||||
queueWrite(new JsonPutRequest(responseObject, this));
|
||||
|
||||
} catch (JSONException e) {
|
||||
logger.error(" JSON exception: ", e);
|
||||
}
|
||||
/*queueWrite(new ActivityFilesGetRequest(this){
|
||||
@Override
|
||||
public void handleFileData(byte[] fileData) {
|
||||
super.handleFileData(fileData);
|
||||
File activityDir = new File(getContext().getExternalFilesDir(null), "activity_hr");
|
||||
activityDir.mkdir();
|
||||
File f = new File(activityDir, String.valueOf(System.currentTimeMillis()));
|
||||
try {
|
||||
f.createNewFile();
|
||||
FileOutputStream fos = new FileOutputStream(f);
|
||||
fos.write(fileData);
|
||||
fos.close();
|
||||
GB.toast("saved file data", Toast.LENGTH_SHORT, GB.INFO);
|
||||
} catch (IOException e) {
|
||||
GB.log("activity file error", GB.ERROR, e);
|
||||
}
|
||||
queueWrite(new FileDeleteRequest((short) 0x0101));
|
||||
}
|
||||
});*/
|
||||
}
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void onInstallApp(Uri uri) {
|
||||
@ -718,6 +782,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public byte[] getSecretKey() {
|
||||
byte[] authKeyBytes = new byte[16];
|
||||
|
||||
|
@ -0,0 +1,65 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.parser;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
public class ActivityEntry {
|
||||
public int id;
|
||||
public int heartRate;
|
||||
public int variability, maxVariability;
|
||||
public int calories;
|
||||
public int stepCount;
|
||||
public boolean isActive;
|
||||
|
||||
public int timestamp;
|
||||
|
||||
public int heartRateQuality;
|
||||
|
||||
public WEARING_STATE wearingState;
|
||||
|
||||
public HybridHRActivitySample toDAOActivitySample(long deviceId){
|
||||
HybridHRActivitySample sample = new HybridHRActivitySample(
|
||||
timestamp,
|
||||
deviceId,
|
||||
-1,
|
||||
stepCount,
|
||||
calories,
|
||||
variability,
|
||||
maxVariability,
|
||||
heartRateQuality,
|
||||
isActive,
|
||||
wearingState.value,
|
||||
heartRate
|
||||
);
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
public enum WEARING_STATE{
|
||||
WEARING((byte) 0, ActivityKind.TYPE_NOT_MEASURED),
|
||||
NOT_WEARING((byte) 1, ActivityKind.TYPE_NOT_WORN),
|
||||
UNKNOWN((byte) 2, ActivityKind.TYPE_UNKNOWN);
|
||||
|
||||
byte value;
|
||||
int activityKind;
|
||||
|
||||
WEARING_STATE(byte value, int activityKind){
|
||||
this.value = value;
|
||||
this.activityKind = activityKind;
|
||||
}
|
||||
|
||||
public int getActivityKind() {
|
||||
return activityKind;
|
||||
}
|
||||
|
||||
static public WEARING_STATE fromValue(byte value){
|
||||
switch (value){
|
||||
case 0: return WEARING_STATE.WEARING;
|
||||
case 1: return WEARING_STATE.NOT_WEARING;
|
||||
case 2: return WEARING_STATE.UNKNOWN;
|
||||
default: throw new RuntimeException("value " + value + " not valid state value");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.parser;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ActivityFileParser {
|
||||
// state flags;
|
||||
int heartRateQuality;
|
||||
ActivityEntry.WEARING_STATE wearingState = ActivityEntry.WEARING_STATE.UNKNOWN;
|
||||
int currentTimestamp = -1;
|
||||
ActivityEntry currentSample = null;
|
||||
int currentId = 1;
|
||||
|
||||
public ArrayList<ActivityEntry> parseFile(byte[] file) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(file);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// read file version
|
||||
short version = buffer.getShort(2);
|
||||
if (version != 22) throw new RuntimeException("File version " + version + ", 16 required");
|
||||
|
||||
int startTime = buffer.getInt(8);
|
||||
short timeOffsetMinutes = buffer.getShort(12);
|
||||
|
||||
short fileId = buffer.getShort(16);
|
||||
|
||||
buffer.position(20);
|
||||
|
||||
ArrayList<ActivityEntry> samples = new ArrayList<>();
|
||||
finishCurrentPacket(samples);
|
||||
|
||||
while (buffer.position() < buffer.capacity() - 4) {
|
||||
byte next = buffer.get();
|
||||
|
||||
if (paraseFlag(next, buffer, samples)) continue;
|
||||
|
||||
if(currentSample != null) {
|
||||
parseVariabilityBytes(next, buffer.get());
|
||||
|
||||
int heartRate = buffer.get() & 0xFF;
|
||||
int calories = buffer.get() & 0xFF;
|
||||
boolean isActive = (calories & 0x40) == 0x40; // upper two bits
|
||||
calories &= 0x3F; // delete upper two bits
|
||||
|
||||
currentSample.heartRate = heartRate;
|
||||
currentSample.calories = calories;
|
||||
currentSample.isActive = isActive;
|
||||
finishCurrentPacket(samples);
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
private boolean paraseFlag(byte flag, ByteBuffer buffer, ArrayList<ActivityEntry> samples) {
|
||||
switch (flag) {
|
||||
case (byte) 0xCA:
|
||||
case (byte) 0xCB:
|
||||
case (byte) 0xCC:
|
||||
case (byte) 0xCD:
|
||||
buffer.get();
|
||||
break;
|
||||
case (byte) 0xCE:
|
||||
byte arg = buffer.get();
|
||||
byte wearBits = (byte)((arg & 0b00011000) >> 3);
|
||||
if(wearBits == 0) this.wearingState = ActivityEntry.WEARING_STATE.NOT_WEARING;
|
||||
else if(wearBits == 1) this.wearingState = ActivityEntry.WEARING_STATE.WEARING;
|
||||
else this.wearingState = ActivityEntry.WEARING_STATE.UNKNOWN;
|
||||
|
||||
byte heartRateQualityBits = (byte)((arg & 0b11100000) >> 5);
|
||||
this.heartRateQuality = heartRateQualityBits;
|
||||
break;
|
||||
case (byte) 0xCF:
|
||||
case (byte) 0xDE:
|
||||
case (byte) 0xDF:
|
||||
case (byte) 0xE1:
|
||||
buffer.get();
|
||||
break;
|
||||
case (byte) 0xE2:
|
||||
byte type = buffer.get();
|
||||
int timestamp = buffer.getInt();
|
||||
short duration = buffer.getShort();
|
||||
short minutesOffset = buffer.getShort();
|
||||
if (type == 0x04) {
|
||||
this.currentTimestamp = timestamp;
|
||||
}
|
||||
break;
|
||||
case (byte) 0xDD:
|
||||
case (byte) 0xFD:
|
||||
buffer.get();
|
||||
break;
|
||||
case (byte) 0xFE:
|
||||
byte arg2 = buffer.get();
|
||||
if(arg2 == (byte) 0xFE) {
|
||||
// this.currentSample = new ActivitySample();
|
||||
// this.currentSample.id = currentId++;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void parseVariabilityBytes(byte lower, byte higher){
|
||||
if((lower & 0b0000001) == 0b0000001){
|
||||
currentSample.maxVariability = (higher & 0b00000011) * 25 + 1;
|
||||
currentSample.stepCount = lower & 0b1110;
|
||||
if((lower & 0b10000000) == 0b10000000){
|
||||
int factor = (lower >> 4) & 0b111;
|
||||
currentSample.variability = 512 + factor * 64 + (higher >> 2 & 0b111111);
|
||||
currentSample.stepCount = lower & 0b1110;
|
||||
}else {
|
||||
currentSample.variability = lower & 0b01110000;
|
||||
currentSample.variability <<= 2;
|
||||
currentSample.variability |= (higher >> 2) & 0b111111;
|
||||
}
|
||||
}else{
|
||||
currentSample.variability = (int) higher * (int) higher * 64;
|
||||
currentSample.maxVariability = 10000;
|
||||
}
|
||||
}
|
||||
|
||||
private void finishCurrentPacket(ArrayList<ActivityEntry> samples) {
|
||||
if (currentSample != null) {
|
||||
currentSample.timestamp = currentTimestamp;
|
||||
currentSample.heartRateQuality = this.heartRateQuality;
|
||||
currentSample.wearingState = wearingState;
|
||||
currentTimestamp += 60;
|
||||
samples.add(currentSample);
|
||||
currentSample = null;
|
||||
}
|
||||
this.currentSample = new ActivityEntry();
|
||||
this.currentSample.id = currentId++;
|
||||
}
|
||||
}
|
@ -16,12 +16,15 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
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.FileGetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupAndGetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class AlarmsGetRequest extends FileLookupAndGetRequest {
|
||||
public AlarmsGetRequest(FossilWatchAdapter adapter) {
|
||||
@ -59,4 +62,13 @@ public class AlarmsGetRequest extends FileLookupAndGetRequest {
|
||||
alarms2[i] = Alarm.fromBytes(alarms[i].getData());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFileLookupError(FILE_LOOKUP_ERROR error) {
|
||||
if(error == FILE_LOOKUP_ERROR.FILE_EMPTY){
|
||||
GB.toast("alarm file empty yet", Toast.LENGTH_LONG, GB.ERROR);
|
||||
}else{
|
||||
throw new RuntimeException("strange lookup stuff");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,17 +20,14 @@ import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
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.devices.qhybrid.QHybridSupport;
|
||||
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.service.devices.qhybrid.requests.fossil_hr.file.ResultCode;
|
||||
|
||||
public class FileLookupRequest extends FossilRequest {
|
||||
public abstract class FileLookupRequest extends FossilRequest {
|
||||
private short handle = -1;
|
||||
private byte fileType;
|
||||
|
||||
@ -91,6 +88,11 @@ public class FileLookupRequest extends FossilRequest {
|
||||
// throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
|
||||
}
|
||||
log("file size: " + size);
|
||||
if(size == 0){
|
||||
this.handleFileLookupError(FILE_LOOKUP_ERROR.FILE_EMPTY);
|
||||
finished = true;
|
||||
return;
|
||||
}
|
||||
fileBuffer = ByteBuffer.allocate(size);
|
||||
}else if((first & 0x0F) == 8){
|
||||
this.finished = true;
|
||||
@ -122,7 +124,9 @@ public class FileLookupRequest extends FossilRequest {
|
||||
}
|
||||
}
|
||||
|
||||
public void handleFileLookup(short fileHandle){}
|
||||
public abstract void handleFileLookup(short fileHandle);
|
||||
|
||||
public abstract void handleFileLookupError(FILE_LOOKUP_ERROR error);
|
||||
|
||||
@Override
|
||||
public UUID getRequestUUID() {
|
||||
@ -138,4 +142,8 @@ public class FileLookupRequest extends FossilRequest {
|
||||
public int getPayloadLength() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public enum FILE_LOOKUP_ERROR{
|
||||
FILE_EMPTY;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.activity;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FileEncryptedGetRequest;
|
||||
|
||||
public class ActivityFilesGetRequest extends FileEncryptedGetRequest {
|
||||
public ActivityFilesGetRequest(FossilHRWatchAdapter adapter) {
|
||||
super((short) 0x0101, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFileData(byte[] fileData) {
|
||||
assert Boolean.TRUE;
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
@ -54,4 +56,13 @@ public class ConfigurationGetRequest extends FileEncryptedLookupAndGetRequest {
|
||||
|
||||
device.sendDeviceUpdateIntent(getAdapter().getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFileLookupError(FILE_LOOKUP_ERROR error) {
|
||||
if(error == FILE_LOOKUP_ERROR.FILE_EMPTY){
|
||||
GB.toast("config file empty", Toast.LENGTH_LONG, GB.ERROR);
|
||||
}else{
|
||||
throw new RuntimeException("strange lookup stuff");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ import java.nio.ByteOrder;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
@ -34,11 +33,10 @@ import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
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.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CRC32C;
|
||||
|
||||
public abstract class FileEncryptedGetRequest extends FossilRequest {
|
||||
private short handle;
|
||||
@ -50,6 +48,10 @@ public abstract class FileEncryptedGetRequest extends FossilRequest {
|
||||
|
||||
private boolean finished = false;
|
||||
|
||||
private Cipher cipher;
|
||||
private SecretKeySpec keySpec;
|
||||
private byte[] iv;
|
||||
|
||||
public FileEncryptedGetRequest(short handle, FossilHRWatchAdapter adapter) {
|
||||
this.handle = handle;
|
||||
this.adapter = adapter;
|
||||
@ -62,12 +64,42 @@ public abstract class FileEncryptedGetRequest extends FossilRequest {
|
||||
.array();
|
||||
}
|
||||
|
||||
private void initDecryption() {
|
||||
try {
|
||||
cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
keySpec = new SecretKeySpec(this.adapter.getSecretKey(), "AES");
|
||||
|
||||
|
||||
iv = new byte[16];
|
||||
|
||||
|
||||
byte[] phoneRandomNumber = adapter.getPhoneRandomNumber();
|
||||
byte[] watchRandomNumber = adapter.getWatchRandomNumber();
|
||||
|
||||
System.arraycopy(phoneRandomNumber, 0, iv, 2, 6);
|
||||
System.arraycopy(watchRandomNumber, 0, iv, 9, 7);
|
||||
|
||||
iv[7]++;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public FossilWatchAdapter getAdapter() {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private void incrementIV(){
|
||||
ByteBuffer buffer = ByteBuffer.wrap(this.iv);
|
||||
int number = buffer.getInt(12);
|
||||
number += 0x1F;
|
||||
buffer.position(12);
|
||||
buffer.putInt(number);
|
||||
this.iv = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished(){
|
||||
public boolean isFinished() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
@ -75,73 +107,66 @@ public abstract class FileEncryptedGetRequest extends FossilRequest {
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] value = characteristic.getValue();
|
||||
byte first = value[0];
|
||||
if(characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")){
|
||||
if((first & 0x0F) == 1){
|
||||
if (characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")) {
|
||||
if ((first & 0x0F) == 1) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
this.initDecryption();
|
||||
|
||||
short handle = buffer.getShort(1);
|
||||
int size = buffer.getInt(4);
|
||||
|
||||
byte status = buffer.get(3);
|
||||
|
||||
ResultCode code = ResultCode.fromCode(status);
|
||||
if(!code.inidicatesSuccess()){
|
||||
if (!code.inidicatesSuccess()) {
|
||||
throw new RuntimeException("FileGet error: " + code + " (" + status + ")");
|
||||
}
|
||||
|
||||
if(this.handle != handle){
|
||||
if (this.handle != handle) {
|
||||
throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
|
||||
}
|
||||
log("file size: " + size);
|
||||
fileBuffer = ByteBuffer.allocate(size);
|
||||
}else if((first & 0x0F) == 8){
|
||||
} else if ((first & 0x0F) == 8) {
|
||||
this.finished = true;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
short handle = buffer.getShort(1);
|
||||
if(this.handle != handle){
|
||||
if (this.handle != handle) {
|
||||
throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
|
||||
}
|
||||
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(this.fileData);
|
||||
|
||||
CRC32C c = new CRC32C();
|
||||
c.update(this.fileData, 0, fileData.length);
|
||||
|
||||
int crcExpected = buffer.getInt(8);
|
||||
|
||||
if((int) crc.getValue() != crcExpected){
|
||||
if ((int) crc.getValue() != crcExpected) {
|
||||
throw new RuntimeException("crc: " + crc.getValue() + " expected: " + crcExpected);
|
||||
}
|
||||
|
||||
this.handleFileData(this.fileData);
|
||||
}
|
||||
}else if(characteristic.getUuid().toString().equals("3dda0004-957f-7d4a-34a6-74696673696d")){
|
||||
SecretKeySpec keySpec = new SecretKeySpec(this.adapter.getSecretKey(), "AES");
|
||||
} else if (characteristic.getUuid().toString().equals("3dda0004-957f-7d4a-34a6-74696673696d")) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
|
||||
byte[] fileIV = new byte[16];
|
||||
|
||||
|
||||
byte[] phoneRandomNumber = adapter.getPhoneRandomNumber();
|
||||
byte[] watchRandomNumber = adapter.getWatchRandomNumber();
|
||||
|
||||
System.arraycopy(phoneRandomNumber, 0, fileIV, 2, 6);
|
||||
System.arraycopy(watchRandomNumber, 0, fileIV, 9, 7);
|
||||
|
||||
fileIV[7]++;
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(fileIV));
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
|
||||
byte[] result = cipher.doFinal(value);
|
||||
|
||||
incrementIV();
|
||||
|
||||
fileBuffer.put(result, 1, result.length - 1);
|
||||
if((result[0] & 0x80) == 0x80){
|
||||
if ((result[0] & 0x80) == 0x80) {
|
||||
this.fileData = fileBuffer.array();
|
||||
}
|
||||
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
|
||||
} catch (BadPaddingException | IllegalBlockSizeException | InvalidAlgorithmParameterException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
@ -51,10 +51,12 @@ import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.isRunningOreoOrLater;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_RECORDED_DATA_TYPES;
|
||||
|
||||
public class GB {
|
||||
|
||||
@ -112,6 +114,7 @@ public class GB {
|
||||
builder.addAction(R.drawable.ic_notification_disconnected, context.getString(R.string.controlcenter_disconnect), disconnectPendingIntent);
|
||||
if (GBApplication.isRunningLollipopOrLater() && DeviceHelper.getInstance().getCoordinator(device).supportsActivityDataFetching()) { //for some reason this fails on KK
|
||||
deviceCommunicationServiceIntent.setAction(DeviceService.ACTION_FETCH_RECORDED_DATA);
|
||||
deviceCommunicationServiceIntent.putExtra(EXTRA_RECORDED_DATA_TYPES, ActivityKind.TYPE_ACTIVITY);
|
||||
PendingIntent fetchPendingIntent = PendingIntent.getService(context, 1, deviceCommunicationServiceIntent, PendingIntent.FLAG_ONE_SHOT);
|
||||
builder.addAction(R.drawable.ic_action_fetch_activity_data, context.getString(R.string.controlcenter_fetch_activity_data), fetchPendingIntent);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user