1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2025-01-23 16:17:32 +01:00

Add Fossil HR Activity Tracking (#1846)

This commit is contained in:
dakhnod 2020-04-20 00:11:45 +02:00 committed by Andreas Shimokawa
parent f30be95209
commit 11d1fd08bd
13 changed files with 522 additions and 65 deletions

View File

@ -43,7 +43,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception { 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 userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes); Entity user = addUserInfo(schema, userAttributes);
@ -71,6 +71,7 @@ public class GBDaoGenerator {
addZeTimeActivitySample(schema, user, device); addZeTimeActivitySample(schema, user, device);
addID115ActivitySample(schema, user, device); addID115ActivitySample(schema, user, device);
addJYouActivitySample(schema, user, device); addJYouActivitySample(schema, user, device);
addHybridHRActivitySample(schema, user, device);
addCalendarSyncState(schema, device); addCalendarSyncState(schema, device);
addAlarms(schema, user, device); addAlarms(schema, user, device);
@ -341,6 +342,23 @@ public class GBDaoGenerator {
return activitySample; 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) { private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
activitySample.setSuperclass(superClass); activitySample.setSuperclass(superClass);
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider"); activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");

View File

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

View File

@ -85,7 +85,7 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
@Override @Override
public boolean supportsActivityTracking() { public boolean supportsActivityTracking() {
return false; return true;
} }
@Override @Override
@ -95,7 +95,7 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
@Override @Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) { public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null; return new HybridHRActivitySampleProvider(device, session);
} }
@Override @Override
@ -132,7 +132,7 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
@Override @Override
public boolean supportsHeartRateMeasurement(GBDevice device) { public boolean supportsHeartRateMeasurement(GBDevice device) {
return false; return this.isHybridHR();
} }
@Override @Override

View File

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

View File

@ -34,11 +34,16 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; 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.GBDeviceEventCallControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HRConfigActivity; 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.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.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; 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.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport; 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.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.RequestMtuRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.SetDeviceStateRequest; 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.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.PlayCallNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayTextNotificationRequest; 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.authentication.VerifyPrivateKeyRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.buttons.ButtonConfigurationPutRequest; 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.ConfigurationGetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration.ConfigurationPutRequest; 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.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.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.AssetImage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.AssetImageFactory; 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.CustomWidgetElement;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.Widget; 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.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.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -488,6 +501,64 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
@Override @Override
public void onFetchActivityData() { public void onFetchActivityData() {
syncSettings(); 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() { 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 @Override
public void onTestNewFunction() { public void onTestNewFunction() {
long ts = System.currentTimeMillis(); /*queueWrite(new ActivityFilesGetRequest(this){
ts /= 1000; @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 { try {
JSONObject responseObject = new JSONObject() f.createNewFile();
.put("res", new JSONObject() FileOutputStream fos = new FileOutputStream(f);
.put("id", 0) // seems the id does not matter? fos.write(fileData);
.put("set", new JSONObject() fos.close();
.put("weatherInfo", new JSONObject() GB.toast("saved file data", Toast.LENGTH_SHORT, GB.INFO);
.put("alive", ts + 60 * 60) } catch (IOException e) {
.put("unit", "c") GB.log("activity file error", GB.ERROR, e);
.put("temp", i)
.put("cond_id", i++)
)
));
queueWrite(new JsonPutRequest(responseObject, this));
} catch (JSONException e) {
logger.error(" JSON exception: ", e);
} }
queueWrite(new FileDeleteRequest((short) 0x0101));
}
});*/
} }
*/
@Override @Override
public void onInstallApp(Uri uri) { public void onInstallApp(Uri uri) {
@ -718,6 +782,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
} }
} }
public byte[] getSecretKey() { public byte[] getSecretKey() {
byte[] authKeyBytes = new byte[16]; byte[] authKeyBytes = new byte[16];

View File

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

View File

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

View File

@ -16,12 +16,15 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm; package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm;
import android.widget.Toast;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; 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.FileGetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupAndGetRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupAndGetRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class AlarmsGetRequest extends FileLookupAndGetRequest { public class AlarmsGetRequest extends FileLookupAndGetRequest {
public AlarmsGetRequest(FossilWatchAdapter adapter) { public AlarmsGetRequest(FossilWatchAdapter adapter) {
@ -59,4 +62,13 @@ public class AlarmsGetRequest extends FileLookupAndGetRequest {
alarms2[i] = Alarm.fromBytes(alarms[i].getData()); 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");
}
}
} }

View File

@ -20,17 +20,14 @@ import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.UUID; import java.util.UUID;
import java.util.zip.CRC32; 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.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.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.ResultCode; 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 short handle = -1;
private byte fileType; private byte fileType;
@ -91,6 +88,11 @@ public class FileLookupRequest extends FossilRequest {
// throw new RuntimeException("handle: " + handle + " expected: " + this.handle); // throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
} }
log("file size: " + size); log("file size: " + size);
if(size == 0){
this.handleFileLookupError(FILE_LOOKUP_ERROR.FILE_EMPTY);
finished = true;
return;
}
fileBuffer = ByteBuffer.allocate(size); fileBuffer = ByteBuffer.allocate(size);
}else if((first & 0x0F) == 8){ }else if((first & 0x0F) == 8){
this.finished = true; 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 @Override
public UUID getRequestUUID() { public UUID getRequestUUID() {
@ -138,4 +142,8 @@ public class FileLookupRequest extends FossilRequest {
public int getPayloadLength() { public int getPayloadLength() {
return 3; return 3;
} }
public enum FILE_LOOKUP_ERROR{
FILE_EMPTY;
}
} }

View File

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

View File

@ -1,5 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration; 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.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
@ -54,4 +56,13 @@ public class ConfigurationGetRequest extends FileEncryptedLookupAndGetRequest {
device.sendDeviceUpdateIntent(getAdapter().getContext()); 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");
}
}
} }

View File

@ -23,7 +23,6 @@ import java.nio.ByteOrder;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.UUID; import java.util.UUID;
import java.util.zip.CRC32; import java.util.zip.CRC32;
@ -34,11 +33,10 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; 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.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter; 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.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.util.CRC32C;
public abstract class FileEncryptedGetRequest extends FossilRequest { public abstract class FileEncryptedGetRequest extends FossilRequest {
private short handle; private short handle;
@ -50,6 +48,10 @@ public abstract class FileEncryptedGetRequest extends FossilRequest {
private boolean finished = false; private boolean finished = false;
private Cipher cipher;
private SecretKeySpec keySpec;
private byte[] iv;
public FileEncryptedGetRequest(short handle, FossilHRWatchAdapter adapter) { public FileEncryptedGetRequest(short handle, FossilHRWatchAdapter adapter) {
this.handle = handle; this.handle = handle;
this.adapter = adapter; this.adapter = adapter;
@ -62,12 +64,42 @@ public abstract class FileEncryptedGetRequest extends FossilRequest {
.array(); .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() { public FossilWatchAdapter getAdapter() {
return adapter; 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 @Override
public boolean isFinished(){ public boolean isFinished() {
return finished; return finished;
} }
@ -75,73 +107,66 @@ public abstract class FileEncryptedGetRequest extends FossilRequest {
public void handleResponse(BluetoothGattCharacteristic characteristic) { public void handleResponse(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue(); byte[] value = characteristic.getValue();
byte first = value[0]; byte first = value[0];
if(characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")){ if (characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")) {
if((first & 0x0F) == 1){ if ((first & 0x0F) == 1) {
ByteBuffer buffer = ByteBuffer.wrap(value); ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.order(ByteOrder.LITTLE_ENDIAN);
this.initDecryption();
short handle = buffer.getShort(1); short handle = buffer.getShort(1);
int size = buffer.getInt(4); int size = buffer.getInt(4);
byte status = buffer.get(3); byte status = buffer.get(3);
ResultCode code = ResultCode.fromCode(status); ResultCode code = ResultCode.fromCode(status);
if(!code.inidicatesSuccess()){ if (!code.inidicatesSuccess()) {
throw new RuntimeException("FileGet error: " + code + " (" + status + ")"); throw new RuntimeException("FileGet error: " + code + " (" + status + ")");
} }
if(this.handle != handle){ if (this.handle != handle) {
throw new RuntimeException("handle: " + handle + " expected: " + this.handle); throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
} }
log("file size: " + size); log("file size: " + size);
fileBuffer = ByteBuffer.allocate(size); fileBuffer = ByteBuffer.allocate(size);
}else if((first & 0x0F) == 8){ } else if ((first & 0x0F) == 8) {
this.finished = true; this.finished = true;
ByteBuffer buffer = ByteBuffer.wrap(value); ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.order(ByteOrder.LITTLE_ENDIAN);
short handle = buffer.getShort(1); short handle = buffer.getShort(1);
if(this.handle != handle){ if (this.handle != handle) {
throw new RuntimeException("handle: " + handle + " expected: " + this.handle); throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
} }
CRC32 crc = new CRC32(); CRC32 crc = new CRC32();
crc.update(this.fileData); crc.update(this.fileData);
CRC32C c = new CRC32C();
c.update(this.fileData, 0, fileData.length);
int crcExpected = buffer.getInt(8); int crcExpected = buffer.getInt(8);
if((int) crc.getValue() != crcExpected){ if ((int) crc.getValue() != crcExpected) {
throw new RuntimeException("crc: " + crc.getValue() + " expected: " + crcExpected); throw new RuntimeException("crc: " + crc.getValue() + " expected: " + crcExpected);
} }
this.handleFileData(this.fileData); this.handleFileData(this.fileData);
} }
}else if(characteristic.getUuid().toString().equals("3dda0004-957f-7d4a-34a6-74696673696d")){ } else if (characteristic.getUuid().toString().equals("3dda0004-957f-7d4a-34a6-74696673696d")) {
SecretKeySpec keySpec = new SecretKeySpec(this.adapter.getSecretKey(), "AES");
try { try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
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));
byte[] result = cipher.doFinal(value); byte[] result = cipher.doFinal(value);
incrementIV();
fileBuffer.put(result, 1, result.length - 1); fileBuffer.put(result, 1, result.length - 1);
if((result[0] & 0x80) == 0x80){ if ((result[0] & 0x80) == 0x80) {
this.fileData = fileBuffer.array(); this.fileData = fileBuffer.array();
} }
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { } catch (BadPaddingException | IllegalBlockSizeException | InvalidAlgorithmParameterException | InvalidKeyException e) {
e.printStackTrace();
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }

View File

@ -51,10 +51,12 @@ import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.isRunningOreoOrLater; import static nodomain.freeyourgadget.gadgetbridge.GBApplication.isRunningOreoOrLater;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_RECORDED_DATA_TYPES;
public class GB { public class GB {
@ -112,6 +114,7 @@ public class GB {
builder.addAction(R.drawable.ic_notification_disconnected, context.getString(R.string.controlcenter_disconnect), disconnectPendingIntent); 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 if (GBApplication.isRunningLollipopOrLater() && DeviceHelper.getInstance().getCoordinator(device).supportsActivityDataFetching()) { //for some reason this fails on KK
deviceCommunicationServiceIntent.setAction(DeviceService.ACTION_FETCH_RECORDED_DATA); 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); 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); builder.addAction(R.drawable.ic_action_fetch_activity_data, context.getString(R.string.controlcenter_fetch_activity_data), fetchPendingIntent);
} }