#1846 Add Fossil HR Activity Tracking

Merged
ashimokawa merged 16 commits from fossil-q-hr-activity into master 1 month ago
  1. +19
    -1
      GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
  2. +75
    -0
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRActivitySampleProvider.java
  3. +3
    -3
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java
  4. +24
    -0
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHybridHRActivitySample.java
  5. +91
    -26
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java
  6. +65
    -0
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/parser/ActivityEntry.java
  7. +136
    -0
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/parser/ActivityFileParser.java
  8. +12
    -0
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/alarm/AlarmsGetRequest.java
  9. +13
    -5
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/file/FileLookupRequest.java
  10. +15
    -0
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/activity/ActivityFilesGetRequest.java
  11. +11
    -0
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/configuration/ConfigurationGetRequest.java
  12. +55
    -30
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FileEncryptedGetRequest.java
  13. +3
    -0
      app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java

+ 19
- 1
GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java 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");


+ 75
- 0
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRActivitySampleProvider.java 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);
}
}

+ 3
- 3
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java 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


+ 24
- 0
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractHybridHRActivitySample.java 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;
}
}

+ 91
- 26
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java 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];



+ 65
- 0
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/parser/ActivityEntry.java 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");
}
}
}
}

+ 136
- 0
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/parser/ActivityFileParser.java 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++;
}
}

+ 12
- 0
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/alarm/AlarmsGetRequest.java 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");
}
}
}

+ 13
- 5
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/file/FileLookupRequest.java 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;
}
}

+ 15
- 0
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/activity/ActivityFilesGetRequest.java 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;
}
}

+ 11
- 0
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/configuration/ConfigurationGetRequest.java 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");
}
}
}

+ 55
- 30
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FileEncryptedGetRequest.java 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);
}
}


+ 3
- 0
app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java 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);
}


Loading…
Cancel
Save