1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-02 03:16:07 +02: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 {
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");

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

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.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];

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/>. */
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");
}
}
}

View File

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

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

View File

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

View File

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