From 830ca1ecbd54b768544d7ea6adecb96cd72a24e6 Mon Sep 17 00:00:00 2001 From: krzys-h Date: Sat, 28 Dec 2019 14:11:42 +0100 Subject: [PATCH] Da Fit: Add activity fetching and logging --- .../gadgetbridge/daogen/GBDaoGenerator.java | 28 ++ .../devices/dafit/DaFitConstants.java | 3 +- .../devices/dafit/DaFitDeviceCoordinator.java | 8 +- .../devices/dafit/DaFitSampleProvider.java | 187 +++++++ .../devices/dafit/DaFitDeviceSupport.java | 472 +++++++++++++++++- .../devices/dafit/FetchDataOperation.java | 196 ++++++++ 6 files changed, 886 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/dafit/FetchDataOperation.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 14159ceae..5b0c7490f 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -49,6 +49,9 @@ public class GBDaoGenerator { private static final String SAMPLE_TEMPERATURE = "temperature"; private static final String SAMPLE_TEMPERATURE_TYPE = "temperatureType"; private static final String SAMPLE_WEIGHT_KG = "weightKg"; + private static final String SAMPLE_BLOOD_PRESSURE_SYSTOLIC = "bloodPressureSystolic"; + private static final String SAMPLE_BLOOD_PRESSURE_DIASTOLIC = "bloodPressureDiastolic"; + private static final String SAMPLE_BLOOD_OXIDATION = "bloodOxidation"; private static final String TIMESTAMP_FROM = "timestampFrom"; private static final String TIMESTAMP_TO = "timestampTo"; @@ -605,6 +608,15 @@ public class GBDaoGenerator { activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE); } + private static void addBloodPressureProperies(Entity activitySample) { + activitySample.addIntProperty(SAMPLE_BLOOD_PRESSURE_SYSTOLIC).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_BLOOD_PRESSURE_DIASTOLIC).notNull().codeBeforeGetterAndSetter(OVERRIDE); + } + + private static void addBloodOxidationProperies(Entity activitySample) { + activitySample.addIntProperty(SAMPLE_BLOOD_OXIDATION).notNull().codeBeforeGetterAndSetter(OVERRIDE); + } + private static Entity addPebbleHealthActivitySample(Schema schema, Entity user, Entity device) { Entity activitySample = addEntity(schema, "PebbleHealthActivitySample"); addCommonActivitySampleProperties("AbstractPebbleHealthActivitySample", activitySample, user, device); @@ -1059,6 +1071,22 @@ public class GBDaoGenerator { return activitySample; } + private static Entity addDaFitActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "DaFitActivitySample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE).codeBeforeGetter("@Override\n public int getRawIntensity() {\n return getSteps();\n }\n\n"); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty("dataSource").notNull(); + activitySample.addIntProperty("caloriesBurnt").notNull(); + activitySample.addIntProperty("distanceMeters").notNull(); + addHeartRateProperties(activitySample); + addBloodPressureProperies(activitySample); + addBloodOxidationProperies(activitySample); + activitySample.addIntProperty("batteryLevel").notNull(); + return activitySample; + } + private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) { activitySample.setSuperclass(superClass); activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitConstants.java index 11149bd6f..7b86ce2e3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitConstants.java @@ -137,7 +137,8 @@ public class DaFitConstants { public static final byte CMD_SYNC_SLEEP = 50; // {} -> {type, start_h, start_m}, repeating, type is SOBER(0),LIGHT(1),RESTFUL(2) public static final byte CMD_SYNC_PAST_SLEEP_AND_STEP = 51; // {b (see below)} -> {x<=2, distance:uint24, steps:uint24, calories:uint24} or {x>2, (sleep data like above)} - two functions same CMD - + + // NOTE: these names are as specified in the original app. They do NOT match what my watch actually does. See note in FetchDataOperation. public static final byte ARG_SYNC_YESTERDAY_STEPS = 1; public static final byte ARG_SYNC_DAY_BEFORE_YESTERDAY_STEPS = 2; public static final byte ARG_SYNC_YESTERDAY_SLEEP = 3; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitDeviceCoordinator.java index 77c2c7f19..cc1a484bd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitDeviceCoordinator.java @@ -90,17 +90,17 @@ public class DaFitDeviceCoordinator extends AbstractDeviceCoordinator { @Override public boolean supportsActivityDataFetching() { - return false; + return true; } @Override public boolean supportsActivityTracking() { - return false; + return true; } @Override public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { - return null; + return new DaFitSampleProvider(device, session); } @Override @@ -145,7 +145,7 @@ public class DaFitDeviceCoordinator extends AbstractDeviceCoordinator { @Override public boolean supportsRealtimeData() { - return false; + return true; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitSampleProvider.java new file mode 100644 index 000000000..c9f282abe --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/dafit/DaFitSampleProvider.java @@ -0,0 +1,187 @@ +/* Copyright (C) 2019 krzys_h + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.dafit; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import de.greenrobot.dao.internal.SqlUtils; +import de.greenrobot.dao.query.WhereCondition; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.DaFitActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.DaFitActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; + +public class DaFitSampleProvider extends AbstractSampleProvider { + public static final int SOURCE_NOT_MEASURED = -1; + public static final int SOURCE_STEPS_REALTIME = 1; // steps gathered at realtime from the steps characteristic + public static final int SOURCE_STEPS_SUMMARY = 2; // steps gathered from the daily summary + public static final int SOURCE_STEPS_IDLE = 3; // idle sample inserted because the user was not moving (to differentiate from missing data because watch not connected) + public static final int SOURCE_SLEEP_SUMMARY = 4; // data collected from the sleep function + public static final int SOURCE_SINGLE_MEASURE = 5; // heart rate / blood data gathered from the "single measurement" function + public static final int SOURCE_TRAINING_HEARTRATE = 6; // heart rate data collected from the training function + public static final int SOURCE_BATTERY = 7; // battery report + + public static final int ACTIVITY_NOT_MEASURED = -1; + public static final int ACTIVITY_TRAINING_WALK = DaFitConstants.TRAINING_TYPE_WALK; + public static final int ACTIVITY_TRAINING_RUN = DaFitConstants.TRAINING_TYPE_RUN; + public static final int ACTIVITY_TRAINING_BIKING = DaFitConstants.TRAINING_TYPE_BIKING; + public static final int ACTIVITY_TRAINING_ROPE = DaFitConstants.TRAINING_TYPE_ROPE; + public static final int ACTIVITY_TRAINING_BADMINTON = DaFitConstants.TRAINING_TYPE_BADMINTON; + public static final int ACTIVITY_TRAINING_BASKETBALL = DaFitConstants.TRAINING_TYPE_BASKETBALL; + public static final int ACTIVITY_TRAINING_FOOTBALL = DaFitConstants.TRAINING_TYPE_FOOTBALL; + public static final int ACTIVITY_TRAINING_SWIM = DaFitConstants.TRAINING_TYPE_SWIM; + public static final int ACTIVITY_TRAINING_MOUNTAINEERING = DaFitConstants.TRAINING_TYPE_MOUNTAINEERING; + public static final int ACTIVITY_TRAINING_TENNIS = DaFitConstants.TRAINING_TYPE_TENNIS; + public static final int ACTIVITY_TRAINING_RUGBY = DaFitConstants.TRAINING_TYPE_RUGBY; + public static final int ACTIVITY_TRAINING_GOLF = DaFitConstants.TRAINING_TYPE_GOLF; + public static final int ACTIVITY_SLEEP_LIGHT = 16; + public static final int ACTIVITY_SLEEP_RESTFUL = 17; + public static final int ACTIVITY_SLEEP_START = 18; + public static final int ACTIVITY_SLEEP_END = 19; + + public DaFitSampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getDaFitActivitySampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return DaFitActivitySampleDao.Properties.Timestamp; + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return DaFitActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return DaFitActivitySampleDao.Properties.DeviceId; + } + + @Override + public DaFitActivitySample createActivitySample() { + return new DaFitActivitySample(); + } + + @Override + public int normalizeType(int rawType) { + if (rawType == ACTIVITY_NOT_MEASURED) + return ActivityKind.TYPE_NOT_MEASURED; + else if (rawType == ACTIVITY_SLEEP_LIGHT) + return ActivityKind.TYPE_LIGHT_SLEEP; + else if (rawType == ACTIVITY_SLEEP_RESTFUL) + return ActivityKind.TYPE_DEEP_SLEEP; + else if (rawType == ACTIVITY_SLEEP_START || rawType == ACTIVITY_SLEEP_END) + return ActivityKind.TYPE_NOT_MEASURED; + else + return ActivityKind.TYPE_ACTIVITY; + } + + @Override + public int toRawActivityKind(int activityKind) { + if (activityKind == ActivityKind.TYPE_NOT_MEASURED) + return ACTIVITY_NOT_MEASURED; + else if (activityKind == ActivityKind.TYPE_LIGHT_SLEEP) + return ACTIVITY_SLEEP_LIGHT; + else if (activityKind == ActivityKind.TYPE_DEEP_SLEEP) + return ACTIVITY_SLEEP_RESTFUL; + else if (activityKind == ActivityKind.TYPE_ACTIVITY) + return ACTIVITY_NOT_MEASURED; // TODO: ? + else + throw new IllegalArgumentException("Invalid Gadgetbridge activity kind: " + activityKind); + } + + @Override + public float normalizeIntensity(int rawIntensity) { + if (rawIntensity == ActivitySample.NOT_MEASURED) + return Float.NEGATIVE_INFINITY; + else + return rawIntensity; + } + + /** + * Set the activity kind from NOT_MEASURED to new_raw_activity_kind on the given range + * @param timestamp_from the start timestamp + * @param timestamp_to the end timestamp + * @param new_raw_activity_kind the activity kind to set + */ + public void updateActivityInRange(int timestamp_from, int timestamp_to, int new_raw_activity_kind) + { + // greenDAO does not provide a bulk update functionality, and manual update fails because + // of no primary key + + Property timestampProperty = getTimestampSampleProperty(); + Device dbDevice = DBHelper.findDevice(getDevice(), getSession()); + if (dbDevice == null) + throw new IllegalStateException(); + Property deviceProperty = getDeviceIdentifierSampleProperty(); + + /*QueryBuilder qb = getSampleDao().queryBuilder(); + qb.where(deviceProperty.eq(dbDevice.getId())) + .where(timestampProperty.ge(timestamp_from), timestampProperty.le(timestamp_to)) + .where(getRawKindSampleProperty().eq(ACTIVITY_NOT_MEASURED)); + List samples = qb.build().list(); + for (DaFitActivitySample sample : samples) { + sample.setProvider(this); + sample.setRawKind(new_raw_activity_kind); + sample.update(); + }*/ + + String tablename = getSampleDao().getTablename(); + String baseSql = SqlUtils.createSqlUpdate(tablename, new String[] { getRawKindSampleProperty().columnName }, new String[] { }); + StringBuilder builder = new StringBuilder(baseSql); + + List values = new ArrayList<>(); + values.add(new_raw_activity_kind); + List whereConditions = new ArrayList<>(); + whereConditions.add(deviceProperty.eq(dbDevice.getId())); + whereConditions.add(timestampProperty.ge(timestamp_from)); + whereConditions.add(timestampProperty.le(timestamp_to)); + whereConditions.add(getRawKindSampleProperty().eq(ACTIVITY_NOT_MEASURED)); + + ListIterator iter = whereConditions.listIterator(); + while (iter.hasNext()) { + if (iter.hasPrevious()) { + builder.append(" AND "); + } + WhereCondition condition = iter.next(); + condition.appendTo(builder, tablename); + condition.appendValuesTo(values); + } + getSampleDao().getDatabase().execSQL(builder.toString(), values.toArray()); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/dafit/DaFitDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/dafit/DaFitDeviceSupport.java index d55166a25..5331a0ed7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/dafit/DaFitDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/dafit/DaFitDeviceSupport.java @@ -20,8 +20,12 @@ import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.Intent; import android.net.Uri; +import android.os.Handler; import android.util.Log; import android.util.Pair; +import android.widget.Toast; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,24 +33,35 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import java.util.Objects; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.Logging; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.devices.dafit.DaFitConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.dafit.DaFitSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.DaFitActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; @@ -58,6 +73,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.Batter import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.HeartRateProfile; +import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; @@ -66,6 +82,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(DaFitDeviceSupport.class); + private static final long IDLE_STEPS_INTERVAL = 5 * 60 * 1000; private final DeviceInfoProfile deviceInfoProfile; private final BatteryInfoProfile batteryInfoProfile; @@ -85,11 +102,16 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport { } }; + private Handler idleUpdateHandler = new Handler(); + public static final int MTU = 20; // TODO: there seems to be some way to change this value...? private DaFitPacketIn packetIn = new DaFitPacketIn(); + private boolean realTimeHeartRate; + public DaFitDeviceSupport() { super(LOG); + batteryCmd.level = ActivitySample.NOT_MEASURED; addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); @@ -124,6 +146,12 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport { return builder; } + @Override + public void dispose() { + super.dispose(); + idleUpdateHandler.removeCallbacks(updateIdleStepsRunnable); + } + private BluetoothGattCharacteristic getTargetCharacteristicForPacketType(byte packetType) { if (packetType == 1) @@ -152,6 +180,7 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport { { byte[] payload = characteristic.getValue(); Log.i("AAAAAAAAAAAAAAAA", "Update step count: " + Logging.formatBytes(characteristic.getValue())); + handleStepsHistory(0, payload, true); return true; } if (charUuid.equals(DaFitConstants.UUID_CHARACTERISTIC_DATA_IN)) @@ -181,6 +210,28 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport { int heartRate = payload[0]; Log.i("XXXXXXXX", "Measure heart rate finished: " + heartRate + " BPM"); + DaFitActivitySample sample = new DaFitActivitySample(); + sample.setTimestamp((int) (System.currentTimeMillis() / 1000)); + + sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED); + sample.setDataSource(DaFitSampleProvider.SOURCE_SINGLE_MEASURE); + + sample.setBatteryLevel(ActivitySample.NOT_MEASURED); + sample.setSteps(ActivitySample.NOT_MEASURED); + sample.setDistanceMeters(ActivitySample.NOT_MEASURED); + sample.setCaloriesBurnt(ActivitySample.NOT_MEASURED); + + sample.setHeartRate(heartRate); + sample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED); + sample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED); + sample.setBloodOxidation(ActivitySample.NOT_MEASURED); + + addGBActivitySample(sample); + broadcastSample(sample); + + if (realTimeHeartRate) + onHeartRateTest(); + return true; } if (packetType == DaFitConstants.CMD_TRIGGER_MEASURE_BLOOD_OXYGEN) @@ -188,6 +239,25 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport { int percent = payload[0]; Log.i("XXXXXXXX", "Measure blood oxygen finished: " + percent + "%"); + DaFitActivitySample sample = new DaFitActivitySample(); + sample.setTimestamp((int) (System.currentTimeMillis() / 1000)); + + sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED); + sample.setDataSource(DaFitSampleProvider.SOURCE_SINGLE_MEASURE); + + sample.setBatteryLevel(ActivitySample.NOT_MEASURED); + sample.setSteps(ActivitySample.NOT_MEASURED); + sample.setDistanceMeters(ActivitySample.NOT_MEASURED); + sample.setCaloriesBurnt(ActivitySample.NOT_MEASURED); + + sample.setHeartRate(ActivitySample.NOT_MEASURED); + sample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED); + sample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED); + sample.setBloodOxidation(percent); + + addGBActivitySample(sample); + broadcastSample(sample); + return true; } if (packetType == DaFitConstants.CMD_TRIGGER_MEASURE_BLOOD_PRESSURE) @@ -197,6 +267,26 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport { int data2 = payload[2]; Log.i("XXXXXXXX", "Measure blood pressure finished: " + data1 + "/" + data2 + " (" + dataUnknown + ")"); + + DaFitActivitySample sample = new DaFitActivitySample(); + sample.setTimestamp((int) (System.currentTimeMillis() / 1000)); + + sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED); + sample.setDataSource(DaFitSampleProvider.SOURCE_SINGLE_MEASURE); + + sample.setBatteryLevel(ActivitySample.NOT_MEASURED); + sample.setSteps(ActivitySample.NOT_MEASURED); + sample.setDistanceMeters(ActivitySample.NOT_MEASURED); + sample.setCaloriesBurnt(ActivitySample.NOT_MEASURED); + + sample.setHeartRate(ActivitySample.NOT_MEASURED); + sample.setBloodPressureSystolic(data1); + sample.setBloodPressureDiastolic(data2); + sample.setBloodOxidation(ActivitySample.NOT_MEASURED); + + addGBActivitySample(sample); + broadcastSample(sample); + return true; } @@ -243,6 +333,37 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport { return false; } + private void addGBActivitySample(DaFitActivitySample sample) { + addGBActivitySamples(new DaFitActivitySample[] { sample }); + } + + private void addGBActivitySamples(DaFitActivitySample[] samples) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + User user = DBHelper.getUser(dbHandler.getDaoSession()); + Device device = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()); + + DaFitSampleProvider provider = new DaFitSampleProvider(getDevice(), dbHandler.getDaoSession()); + + for (DaFitActivitySample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + sample.setProvider(provider); + provider.addGBActivitySample(sample); + } + } catch (Exception ex) { + ex.printStackTrace(); + GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext()); + } + } + + private void broadcastSample(DaFitActivitySample sample) { + Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) + .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample) + .putExtra(DeviceService.EXTRA_TIMESTAMP, sample.getTimestamp()); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + private void handleDeviceInfo(DeviceInfo info) { LOG.warn("Device info: " + info); versionCmd.hwVersion = info.getHardwareRevision(); @@ -254,6 +375,25 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport { LOG.warn("Battery info: " + info); batteryCmd.level = (short) info.getPercentCharged(); handleGBDeviceEvent(batteryCmd); + + DaFitActivitySample sample = new DaFitActivitySample(); + sample.setTimestamp((int) (System.currentTimeMillis() / 1000)); + + sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED); + sample.setDataSource(DaFitSampleProvider.SOURCE_BATTERY); + + sample.setBatteryLevel(batteryCmd.level); + sample.setSteps(ActivitySample.NOT_MEASURED); + sample.setDistanceMeters(ActivitySample.NOT_MEASURED); + sample.setCaloriesBurnt(ActivitySample.NOT_MEASURED); + + sample.setHeartRate(ActivitySample.NOT_MEASURED); + sample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED); + sample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED); + sample.setBloodOxidation(ActivitySample.NOT_MEASURED); + + addGBActivitySample(sample); + broadcastSample(sample); } @Override @@ -373,7 +513,327 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport { @Override public void onFetchRecordedData(int dataTypes) { - // TODO + if ((dataTypes & RecordedDataTypes.TYPE_ACTIVITY) != 0) + { + try { + new FetchDataOperation(this).perform(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private static int BytesToInt24(byte[] bArr) { + if (bArr.length != 3) + throw new IllegalArgumentException(); + return ((bArr[2] << 24) >>> 8) | ((bArr[1] << 8) & 0xFF00) | (bArr[0] & 0xFF); + } + + private Runnable updateIdleStepsRunnable = new Runnable() { + @Override + public void run() { + try { + updateIdleSteps(); + } finally { + idleUpdateHandler.postDelayed(updateIdleStepsRunnable, IDLE_STEPS_INTERVAL); + } + } + }; + + private void updateIdleSteps() + { + // The steps value hasn't changed for a while, so the user is not moving + // Store this information in the database to improve the averaging over long periods of time + + if (!getDevice().isConnected()) + { + LOG.warn("updateIdleSteps but device not connected?!"); + return; + } + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + User user = DBHelper.getUser(dbHandler.getDaoSession()); + Device device = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()); + + DaFitSampleProvider provider = new DaFitSampleProvider(getDevice(), dbHandler.getDaoSession()); + + int currentSampleTimestamp = (int)(Calendar.getInstance().getTimeInMillis() / 1000); + + DaFitActivitySample sample = new DaFitActivitySample(); + sample.setDevice(device); + sample.setUser(user); + sample.setProvider(provider); + sample.setTimestamp(currentSampleTimestamp); + + sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED); + sample.setDataSource(DaFitSampleProvider.SOURCE_STEPS_IDLE); + + sample.setBatteryLevel(batteryCmd.level); + sample.setSteps(0); + sample.setDistanceMeters(0); + sample.setCaloriesBurnt(0); + + sample.setHeartRate(ActivitySample.NOT_MEASURED); + sample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED); + sample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED); + sample.setBloodOxidation(ActivitySample.NOT_MEASURED); + + provider.addGBActivitySample(sample); + broadcastSample(sample); + + LOG.info("Adding an idle sample: " + sample.toString()); + } catch (Exception ex) { + ex.printStackTrace(); + GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext()); + } + } + + public void handleStepsHistory(int daysAgo, byte[] data, boolean isRealtime) + { + if (data.length != 9) + throw new IllegalArgumentException(); + + byte[] bArr2 = new byte[3]; + System.arraycopy(data, 0, bArr2, 0, 3); + int steps = BytesToInt24(bArr2); + System.arraycopy(data, 3, bArr2, 0, 3); + int distance = BytesToInt24(bArr2); + System.arraycopy(data, 6, bArr2, 0, 3); + int calories = BytesToInt24(bArr2); + + Log.i("steps[" + daysAgo + "]", "steps=" + steps + ", distance=" + distance + ", calories=" + calories); + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + User user = DBHelper.getUser(dbHandler.getDaoSession()); + Device device = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()); + + DaFitSampleProvider provider = new DaFitSampleProvider(getDevice(), dbHandler.getDaoSession()); + + Calendar thisSample = Calendar.getInstance(); + if (daysAgo != 0) + { + thisSample.add(Calendar.DATE, -daysAgo); + thisSample.set(Calendar.HOUR_OF_DAY, 23); + thisSample.set(Calendar.MINUTE, 59); + thisSample.set(Calendar.SECOND, 59); + thisSample.set(Calendar.MILLISECOND, 999); + } + else + { + // no change needed - use current time + } + + Calendar startOfDay = (Calendar) thisSample.clone(); + startOfDay.set(Calendar.HOUR_OF_DAY, 0); + startOfDay.set(Calendar.MINUTE, 0); + startOfDay.set(Calendar.SECOND, 0); + startOfDay.set(Calendar.MILLISECOND, 0); + + int startOfDayTimestamp = (int) (startOfDay.getTimeInMillis() / 1000); + int thisSampleTimestamp = (int) (thisSample.getTimeInMillis() / 1000); + + int previousSteps = 0; + int previousDistance = 0; + int previousCalories = 0; + for (DaFitActivitySample sample : provider.getAllActivitySamples(startOfDayTimestamp, thisSampleTimestamp)) + { + if (sample.getSteps() != ActivitySample.NOT_MEASURED) + previousSteps += sample.getSteps(); + if (sample.getDistanceMeters() != ActivitySample.NOT_MEASURED) + previousDistance += sample.getDistanceMeters(); + if (sample.getCaloriesBurnt() != ActivitySample.NOT_MEASURED) + previousCalories += sample.getCaloriesBurnt(); + } + + int newSteps = steps - previousSteps; + int newDistance = distance - previousDistance; + int newCalories = calories - previousCalories; + + if (newSteps < 0 || newDistance < 0 || newCalories < 0) + { + LOG.warn("Ignoring a sample that would generate negative values: steps += " + newSteps + ", distance +=" + newDistance + ", calories += " + newCalories); + } + else if (newSteps != 0 || newDistance != 0 || newCalories != 0 || daysAgo == 0) + { + DaFitActivitySample sample = new DaFitActivitySample(); + sample.setDevice(device); + sample.setUser(user); + sample.setProvider(provider); + sample.setTimestamp(thisSampleTimestamp); + + sample.setRawKind(DaFitSampleProvider.ACTIVITY_NOT_MEASURED); + sample.setDataSource(daysAgo == 0 ? DaFitSampleProvider.SOURCE_STEPS_REALTIME : DaFitSampleProvider.SOURCE_STEPS_SUMMARY); + + sample.setBatteryLevel(ActivitySample.NOT_MEASURED); + sample.setSteps(newSteps); + sample.setDistanceMeters(newDistance); + sample.setCaloriesBurnt(newCalories); + + sample.setHeartRate(ActivitySample.NOT_MEASURED); + sample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED); + sample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED); + sample.setBloodOxidation(ActivitySample.NOT_MEASURED); + + provider.addGBActivitySample(sample); + if (isRealtime) + { + idleUpdateHandler.removeCallbacks(updateIdleStepsRunnable); + idleUpdateHandler.postDelayed(updateIdleStepsRunnable, IDLE_STEPS_INTERVAL); + broadcastSample(sample); + } + + LOG.info("Adding a sample: " + sample.toString()); + } + } catch (Exception ex) { + ex.printStackTrace(); + GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext()); + } + } + + public void handleSleepHistory(int daysAgo, byte[] data) + { + if (data.length % 3 != 0) + throw new IllegalArgumentException(); + + int prevActivityType = DaFitSampleProvider.ACTIVITY_SLEEP_START; + int prevSampleTimestamp = -1; + + for(int i = 0; i < data.length / 3; i++) + { + int type = data[3*i]; + int start_h = data[3*i + 1]; + int start_m = data[3*i + 2]; + + Log.i("sleep[" + daysAgo + "][" + i + "]", "type=" + type + ", start_h=" + start_h + ", start_m=" + start_m); + + // SleepAnalysis measures sleep fragment type by marking the END of the fragment. + // The watch provides data by marking the START of the fragment. + + // Additionally, ActivityAnalysis (used by the weekly view...) does AVERAGING when + // adjacent samples are not of the same type.. + + // FIXME: The way Gadgetbridge does it seems kinda broken... + + // This means that we have to convert the data when importing. Each sample gets + // converted to two samples - one marking the beginning of the segment, and another + // marking the end. + + // Watch: SLEEP_LIGHT ... SLEEP_DEEP ... SLEEP_LIGHT ... SLEEP_SOBER + // Gadgetbridge: ANYTHING,SLEEP_LIGHT ... SLEEP_LIGHT,SLEEP_DEEP ... SLEEP_DEEP,SLEEP_LIGHT ... SLEEP_LIGHT,ANYTHING + // ^ ^- this is important, it MUST be sleep, to ensure proper detection + // Time since the last -| of sleepStart, see SleepAnalysis.calculateSleepSessions + // sample must be 0 + // (otherwise SleepAnalysis will include this fragment...) + + // This means that when inserting samples: + // * every sample is converted to (previous_sample_type, current_sample_type) happening + // roughly at the same time (but in this order) + // * the first sample is prefixed by unspecified activity + // * the last sample (SOBER) is converted to unspecified activity + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + User user = DBHelper.getUser(dbHandler.getDaoSession()); + Device device = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()); + + DaFitSampleProvider provider = new DaFitSampleProvider(getDevice(), dbHandler.getDaoSession()); + + Calendar thisSample = Calendar.getInstance(); + thisSample.add(Calendar.HOUR_OF_DAY, 4); // the clock assumes the sleep day changes at 20:00, so move the time forward to make the day correct + thisSample.set(Calendar.MINUTE, 0); + thisSample.add(Calendar.DATE, -daysAgo); + + thisSample.set(Calendar.HOUR_OF_DAY, start_h); + thisSample.set(Calendar.MINUTE, start_m); + thisSample.set(Calendar.SECOND, 0); + thisSample.set(Calendar.MILLISECOND, 0); + int thisSampleTimestamp = (int) (thisSample.getTimeInMillis() / 1000); + + int activityType; + if (type == DaFitConstants.SLEEP_SOBER) + activityType = DaFitSampleProvider.ACTIVITY_SLEEP_END; + else if (type == DaFitConstants.SLEEP_LIGHT) + activityType = DaFitSampleProvider.ACTIVITY_SLEEP_LIGHT; + else if (type == DaFitConstants.SLEEP_RESTFUL) + activityType = DaFitSampleProvider.ACTIVITY_SLEEP_RESTFUL; + else + throw new IllegalArgumentException("Invalid sleep type"); + + // Insert the end of previous segment sample + DaFitActivitySample prevSegmentSample = new DaFitActivitySample(); + prevSegmentSample.setDevice(device); + prevSegmentSample.setUser(user); + prevSegmentSample.setProvider(provider); + prevSegmentSample.setTimestamp(thisSampleTimestamp - 1); + + prevSegmentSample.setRawKind(prevActivityType); + prevSegmentSample.setDataSource(DaFitSampleProvider.SOURCE_SLEEP_SUMMARY); + + prevSegmentSample.setBatteryLevel(ActivitySample.NOT_MEASURED); + prevSegmentSample.setSteps(ActivitySample.NOT_MEASURED); + prevSegmentSample.setDistanceMeters(ActivitySample.NOT_MEASURED); + prevSegmentSample.setCaloriesBurnt(ActivitySample.NOT_MEASURED); + + prevSegmentSample.setHeartRate(ActivitySample.NOT_MEASURED); + prevSegmentSample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED); + prevSegmentSample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED); + prevSegmentSample.setBloodOxidation(ActivitySample.NOT_MEASURED); + + addGBActivitySampleIfNotExists(provider, prevSegmentSample); + + // Insert the start of new segment sample + DaFitActivitySample nextSegmentSample = new DaFitActivitySample(); + nextSegmentSample.setDevice(device); + nextSegmentSample.setUser(user); + nextSegmentSample.setProvider(provider); + nextSegmentSample.setTimestamp(thisSampleTimestamp); + + nextSegmentSample.setRawKind(activityType); + nextSegmentSample.setDataSource(DaFitSampleProvider.SOURCE_SLEEP_SUMMARY); + + nextSegmentSample.setBatteryLevel(ActivitySample.NOT_MEASURED); + nextSegmentSample.setSteps(ActivitySample.NOT_MEASURED); + nextSegmentSample.setDistanceMeters(ActivitySample.NOT_MEASURED); + nextSegmentSample.setCaloriesBurnt(ActivitySample.NOT_MEASURED); + + nextSegmentSample.setHeartRate(ActivitySample.NOT_MEASURED); + nextSegmentSample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED); + nextSegmentSample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED); + nextSegmentSample.setBloodOxidation(ActivitySample.NOT_MEASURED); + + addGBActivitySampleIfNotExists(provider, nextSegmentSample); + + // Set the activity type on all samples in this time period + if (prevActivityType != DaFitSampleProvider.ACTIVITY_SLEEP_START) + provider.updateActivityInRange(prevSampleTimestamp, thisSampleTimestamp, prevActivityType); + + prevActivityType = activityType; + if (prevActivityType == DaFitSampleProvider.ACTIVITY_SLEEP_END) + prevActivityType = DaFitSampleProvider.ACTIVITY_SLEEP_START; + prevSampleTimestamp = thisSampleTimestamp; + } catch (Exception ex) { + ex.printStackTrace(); + GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext()); + } + } + } + + private void addGBActivitySampleIfNotExists(DaFitSampleProvider provider, DaFitActivitySample sample) + { + boolean alreadyHaveThisSample = false; + for (DaFitActivitySample sample2 : provider.getAllActivitySamples(sample.getTimestamp() - 1, sample.getTimestamp() + 1)) + { + if (sample2.getTimestamp() == sample2.getTimestamp() && sample2.getRawKind() == sample.getRawKind()) + alreadyHaveThisSample = true; + } + + if (!alreadyHaveThisSample) + { + provider.addGBActivitySample(sample); + LOG.info("Adding a sample: " + sample.toString()); + } } @Override @@ -414,12 +874,18 @@ public class DaFitDeviceSupport extends AbstractBTLEDeviceSupport { @Override public void onEnableRealtimeSteps(boolean enable) { - // TODO + // enabled all the time :D that's the only way to get more than a daily sum from this watch... } @Override public void onEnableRealtimeHeartRateMeasurement(boolean enable) { - // TODO + if (realTimeHeartRate == enable) + return; + realTimeHeartRate = enable; // will do another measurement immediately + if (realTimeHeartRate) + onHeartRateTest(); + else + onAbortHeartRateTest(); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/dafit/FetchDataOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/dafit/FetchDataOperation.java new file mode 100644 index 000000000..da674e1f1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/dafit/FetchDataOperation.java @@ -0,0 +1,196 @@ +/* Copyright (C) 2019 krzys_h + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.dafit; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.util.Log; +import android.util.Pair; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.Logging; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.dafit.DaFitConstants; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class FetchDataOperation extends AbstractBTLEOperation { + + private static final Logger LOG = LoggerFactory.getLogger(FetchDataOperation.class); + + private boolean[] receivedSteps = new boolean[3]; + private boolean[] receivedSleep = new boolean[3]; + + private DaFitPacketIn packetIn = new DaFitPacketIn(); + + public FetchDataOperation(DaFitDeviceSupport support) { + super(support); + } + + @Override + protected void prePerform() { + getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_activity_data)); + getDevice().sendDeviceUpdateIntent(getContext()); + } + + @Override + protected void doPerform() throws IOException { + TransactionBuilder builder = performInitialized("FetchDataOperation"); + getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { DaFitConstants.ARG_SYNC_YESTERDAY_SLEEP })); + getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { DaFitConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_SLEEP })); + getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_SYNC_SLEEP, new byte[0])); + getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { DaFitConstants.ARG_SYNC_YESTERDAY_STEPS })); + getSupport().sendPacket(builder, DaFitPacketOut.buildPacket(DaFitConstants.CMD_SYNC_PAST_SLEEP_AND_STEP, new byte[] { DaFitConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_STEPS })); + builder.read(getCharacteristic(DaFitConstants.UUID_CHARACTERISTIC_STEPS)); + builder.queue(getQueue()); + + updateProgressAndCheckFinish(); + } + + @Override + public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (!isOperationRunning()) + { + LOG.error("onCharacteristicRead but operation is not running!"); + } + else + { + UUID charUuid = characteristic.getUuid(); + if (charUuid.equals(DaFitConstants.UUID_CHARACTERISTIC_STEPS)) { + byte[] data = characteristic.getValue(); + Log.i("TODAY STEPS", "data: " + Logging.formatBytes(data)); + decodeSteps(0, data); + return true; + } + } + + return super.onCharacteristicRead(gatt, characteristic, status); + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + if (!isOperationRunning()) + { + LOG.error("onCharacteristicChanged but operation is not running!"); + } + else + { + UUID charUuid = characteristic.getUuid(); + if (charUuid.equals(DaFitConstants.UUID_CHARACTERISTIC_DATA_IN)) + { + if (packetIn.putFragment(characteristic.getValue())) { + Pair packet = DaFitPacketIn.parsePacket(packetIn.getPacket()); + packetIn = new DaFitPacketIn(); + if (packet != null) { + byte packetType = packet.first; + byte[] payload = packet.second; + + if (handlePacket(packetType, payload)) + return true; + } + } + } + } + + return super.onCharacteristicChanged(gatt, characteristic); + } + + private boolean handlePacket(byte packetType, byte[] payload) { + if (packetType == DaFitConstants.CMD_SYNC_SLEEP) { + Log.i("TODAY SLEEP", "data: " + Logging.formatBytes(payload)); + decodeSleep(0, payload); + return true; + } + if (packetType == DaFitConstants.CMD_SYNC_PAST_SLEEP_AND_STEP) { + byte dataType = payload[0]; + byte[] data = new byte[payload.length - 1]; + System.arraycopy(payload, 1, data, 0, data.length); + + // NOTE: Does this seem swapped to you? That's because IT IS! I took the constant names + // from the official app, but as it turns out, the official app has a bug. + // (and yes, you can see that data from yesterday appears as two days ago + // in the app itself and all past data is getting messed up because of it) + + if (dataType == DaFitConstants.ARG_SYNC_YESTERDAY_STEPS) { + Log.i("2 DAYS AGO STEPS", "data: " + Logging.formatBytes(data)); + decodeSteps(2, data); + return true; + } + else if (dataType == DaFitConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_STEPS) { + Log.i("YESTERDAY STEPS", "data: " + Logging.formatBytes(data)); + decodeSteps(1, data); + return true; + } + else if (dataType == DaFitConstants.ARG_SYNC_YESTERDAY_SLEEP) { + Log.i("2 DAYS AGO SLEEP", "data: " + Logging.formatBytes(data)); + decodeSleep(2, data); + return true; + } + else if (dataType == DaFitConstants.ARG_SYNC_DAY_BEFORE_YESTERDAY_SLEEP) { + Log.i("YESTERDAY SLEEP", "data: " + Logging.formatBytes(data)); + decodeSleep(1, data); + return true; + } + } + return false; + } + + private void decodeSteps(int daysAgo, byte[] data) + { + getSupport().handleStepsHistory(daysAgo, data, false); + receivedSteps[daysAgo] = true; + updateProgressAndCheckFinish(); + } + + private void decodeSleep(int daysAgo, byte[] data) + { + getSupport().handleSleepHistory(daysAgo, data); + receivedSleep[daysAgo] = true; + updateProgressAndCheckFinish(); + } + + private void updateProgressAndCheckFinish() + { + int count = 0; + int total = receivedSteps.length + receivedSleep.length; + for(int i = 0; i < receivedSteps.length; i++) + if (receivedSteps[i]) + ++count; + for(int i = 0; i < receivedSleep.length; i++) + if (receivedSleep[i]) + ++count; + GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, 100 * count / total, getContext()); + if (count == total) + operationFinished(); + } + + @Override + protected void operationFinished() { + operationStatus = OperationStatus.FINISHED; + if (getDevice() != null && getDevice().isConnected()) { + unsetBusy(); + GB.signalActivityDataFinish(); + } + } +}