diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
index 2d45cb09e..bff6e5ae8 100644
--- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
+++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
@@ -43,7 +43,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception {
- final Schema schema = new Schema(43, MAIN_PACKAGE + ".entities");
+ final Schema schema = new Schema(44, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@@ -638,6 +638,7 @@ public class GBDaoGenerator {
summary.addIntProperty("baseAltitude").javaDocGetterAndSetter("Temporary, bip-specific");
summary.addStringProperty("gpxTrack").codeBeforeGetter(OVERRIDE);
+ summary.addStringProperty("rawDetailsPath");
Property deviceId = summary.addLongProperty("deviceId").notNull().codeBeforeGetter(OVERRIDE).getProperty();
summary.addToOne(device, deviceId);
diff --git a/app/build.gradle b/app/build.gradle
index 62e8fb689..e1a3af05b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -336,6 +336,17 @@ tasks.withType(SpotBugsTask) {
}
}
+task cleanGenerated(type: Delete) {
+ delete fileTree('src/main/java/nodomain/freeyourgadget/gadgetbridge/proto') {
+ include '**/*.java'
+ }
+ delete fileTree('src/main/java/nodomain/freeyourgadget/gadgetbridge/entities') {
+ include '**/*.java'
+ exclude '**/Abstract*.java'
+ }
+}
+
+tasks.clean.dependsOn(tasks.cleanGenerated)
protobuf {
protoc {
@@ -344,7 +355,12 @@ protobuf {
generateProtoTasks {
all().each { task ->
task.builtins {
- java { option 'lite' }
+ java {
+ option 'lite'
+ // Uncomment this to get Android Studio to recognize the generated files
+ // this makes it think that all src files are generated though...
+ //outputSubDir = '../../../../../src/main/java/'
+ }
}
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java
index e4cd7c71d..b08f82d7d 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesActivity.java
@@ -17,9 +17,11 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.activities;
+import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
@@ -232,13 +234,25 @@ public class ActivitySummariesActivity extends AbstractListActivity toDelete = new ArrayList<>();
+ final List toDelete = new ArrayList<>();
for (int i = 0; i < checked.size(); i++) {
if (checked.valueAt(i)) {
toDelete.add(getItemAdapter().getItem(checked.keyAt(i)));
}
}
- deleteItems(toDelete);
+
+ new AlertDialog.Builder(ActivitySummariesActivity.this)
+ .setTitle(ActivitySummariesActivity.this.getString(R.string.sports_activity_confirm_delete_title, toDelete.size()))
+ .setMessage(ActivitySummariesActivity.this.getString(R.string.sports_activity_confirm_delete_description, toDelete.size()))
+ .setIcon(R.drawable.ic_delete_forever)
+ .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ public void onClick(final DialogInterface dialog, final int whichButton) {
+ deleteItems(toDelete);
+ }
+ })
+ .setNegativeButton(android.R.string.no, null)
+ .show();
+
processed = true;
break;
case R.id.activity_action_export:
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java
index ee29b3a66..b8bc504c1 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java
@@ -73,6 +73,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
@@ -80,8 +81,10 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryItems;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.SwipeEvents;
@@ -414,13 +417,16 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
}
private void makeSummaryContent(BaseActivitySummary item) {
+ final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
+ final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(gbDevice);
+
//make view of data from summaryData of item
String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
String UNIT_IMPERIAL = GBApplication.getContext().getString(R.string.p_unit_imperial);
TableLayout fieldLayout = findViewById(R.id.summaryDetails);
fieldLayout.removeAllViews(); //remove old widgets
- ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(item);
+ ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, item);
JSONObject data = activitySummaryJsonSummary.getSummaryGroupedList(); //get list, grouped by groups
if (data == null) return;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java
index e95578d5f..7ad46a05b 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java
@@ -874,7 +874,11 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
if (supportedLanguages != null) {
supportedSettings = ArrayUtils.insert(0, supportedSettings, R.xml.devicesettings_language_generic);
}
- supportedSettings = ArrayUtils.addAll(supportedSettings, coordinator.getSupportedDeviceSpecificAuthenticationSettings());
+ final int[] supportedAuthSettings = coordinator.getSupportedDeviceSpecificAuthenticationSettings();
+ if (supportedAuthSettings != null && supportedAuthSettings.length > 0) {
+ supportedSettings = ArrayUtils.add(supportedSettings, R.xml.devicesettings_header_authentication);
+ supportedSettings = ArrayUtils.addAll(supportedSettings, supportedAuthSettings);
+ }
}
final DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer = coordinator.getDeviceSpecificSettingsCustomizer(device);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java
index e21c2dd77..1f5353921 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java
@@ -42,13 +42,16 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -184,6 +187,8 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter. */
+package nodomain.freeyourgadget.gadgetbridge.database.schema;
+
+import android.database.sqlite.SQLiteDatabase;
+
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
+import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
+
+public class GadgetbridgeUpdate_44 implements DBUpdateScript {
+ @Override
+ public void upgradeSchema(final SQLiteDatabase db) {
+ if (!DBHelper.existsColumn(BaseActivitySummaryDao.TABLENAME, BaseActivitySummaryDao.Properties.RawDetailsPath.columnName, db)) {
+ final String statement = "ALTER TABLE " + BaseActivitySummaryDao.TABLENAME + " ADD COLUMN "
+ + BaseActivitySummaryDao.Properties.RawDetailsPath.columnName + " TEXT";
+ db.execSQL(statement);
+ }
+ }
+
+ @Override
+ public void downgradeSchema(final SQLiteDatabase db) {
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
index 1d2e09cf9..748f6c841 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
@@ -46,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.entities.AlarmDao;
import nodomain.freeyourgadget.gadgetbridge.entities.BatteryLevelDao;
@@ -54,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@@ -150,6 +152,12 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return device.isInitialized() && !device.isBusy() && supportsActivityDataFetching();
}
+ @Override
+ @Nullable
+ public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
+ return null;
+ }
+
public boolean isHealthWearable(BluetoothDevice device) {
BluetoothClass bluetoothClass = device.getBluetoothClass();
if (bluetoothClass == null) {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
index cfe26567e..6eee5375d 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
@@ -40,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
@@ -215,6 +216,13 @@ public interface DeviceCoordinator {
*/
SampleProvider extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session);
+ /**
+ * Returns the {@link ActivitySummaryParser} for the device being supported.
+ *
+ * @return
+ */
+ ActivitySummaryParser getActivitySummaryParser(final GBDevice device);
+
/**
* Returns true if this device/coordinator supports installing files like firmware,
* watchfaces, gps, resources, fonts...
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021ActivitySummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021ActivitySummaryParser.java
new file mode 100644
index 000000000..1d587cb31
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021ActivitySummaryParser.java
@@ -0,0 +1,123 @@
+/* Copyright (C) 2022 José Rebelo
+
+ 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.huami;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Date;
+
+import nodomain.freeyourgadget.gadgetbridge.proto.HuamiProtos;
+import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021ActivityDetailsParser;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021WorkoutTrackActivityType;
+
+public class Huami2021ActivitySummaryParser extends HuamiActivitySummaryParser {
+ private static final Logger LOG = LoggerFactory.getLogger(Huami2021ActivitySummaryParser.class);
+
+ @Override
+ public AbstractHuamiActivityDetailsParser getDetailsParser(final BaseActivitySummary summary) {
+ return new Huami2021ActivityDetailsParser(summary);
+ }
+
+ @Override
+ protected void parseBinaryData(final BaseActivitySummary summary, final Date startTime) {
+ final byte[] rawData = summary.getRawSummaryData();
+ final int version = (rawData[0] & 0xff) | ((rawData[1] & 0xff) << 8);
+ if (version != 0x8000) {
+ LOG.warn("Unexpected binary data version {}, parsing might fail", version);
+ }
+
+ final byte[] protobufData = ArrayUtils.subarray(rawData, 2, rawData.length);
+ final HuamiProtos.WorkoutSummary summaryProto;
+ try {
+ summaryProto = HuamiProtos.WorkoutSummary.parseFrom(protobufData);
+ } catch (final InvalidProtocolBufferException e) {
+ LOG.error("Failed to parse summary protobuf data", e);
+ return;
+ }
+
+ if (summaryProto.hasType()) {
+ final Huami2021WorkoutTrackActivityType workoutTrackActivityType = Huami2021WorkoutTrackActivityType
+ .fromCode((byte) summaryProto.getType().getType());
+
+ final int activityKind;
+ if (workoutTrackActivityType != null) {
+ activityKind = workoutTrackActivityType.toActivityKind();
+ } else {
+ LOG.warn("Unknown workout activity type code {}", String.format("0x%X", summaryProto.getType().getType()));
+ activityKind = ActivityKind.TYPE_UNKNOWN;
+ }
+ summary.setActivityKind(activityKind);
+ }
+
+ if (summaryProto.hasTime()) {
+ int totalDuration = summaryProto.getTime().getTotalDuration();
+ summary.setEndTime(new Date(startTime.getTime() + totalDuration * 1000L));
+ addSummaryData("activeSeconds", summaryProto.getTime().getWorkoutDuration(), "seconds");
+ // TODO pause durations
+ }
+
+ if (summaryProto.hasLocation()) {
+ summary.setBaseLongitude(summaryProto.getLocation().getBaseLongitude());
+ summary.setBaseLatitude(summaryProto.getLocation().getBaseLatitude());
+ summary.setBaseAltitude(summaryProto.getLocation().getBaseAltitude() / 2);
+ // TODO: Min/Max Latitude/Longitude
+ }
+
+ if (summaryProto.hasHeartRate()) {
+ addSummaryData("averageHR", summaryProto.getHeartRate().getAvg(), "bpm");
+ addSummaryData("maxHR", summaryProto.getHeartRate().getMax(), "bpm");
+ addSummaryData("minHR", summaryProto.getHeartRate().getMin(), "bpm");
+ }
+
+ if (summaryProto.hasSteps()) {
+ addSummaryData("maxCadence", summaryProto.getSteps().getMaxCadence() * 60, "spm");
+ addSummaryData("averageCadence", summaryProto.getSteps().getAvgCadence() * 60, "spm");
+ addSummaryData("averageStride", summaryProto.getSteps().getAvgStride(), "cm");
+ addSummaryData("steps", summaryProto.getSteps().getSteps(), "steps_unit");
+ }
+
+ if (summaryProto.hasDistance()) {
+ addSummaryData("distanceMeters", summaryProto.getDistance().getDistance(), "meters");
+ }
+
+ if (summaryProto.hasPace()) {
+ addSummaryData("maxPace", summaryProto.getPace().getBest(), "seconds_m");
+ addSummaryData("averageKMPaceSeconds", summaryProto.getPace().getAvg() * 1000, "seconds_km");
+ }
+
+ if (summaryProto.hasCalories()) {
+ addSummaryData("caloriesBurnt", summaryProto.getCalories().getCalories(), "calories_unit");
+ }
+
+ if (summaryProto.hasHeartRateZones()) {
+ // TODO HR zones
+ }
+
+ if (summaryProto.hasTrainingEffect()) {
+ // TODO training effect
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java
index 80cb6f9de..ef7954f07 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java
@@ -33,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
public abstract class Huami2021Coordinator extends HuamiCoordinator {
@Override
@@ -68,8 +69,7 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
@Override
public boolean supportsActivityTracks() {
- // TODO: It's supported by the devices, but not yet implemented
- return false;
+ return true;
}
@Override
@@ -102,6 +102,11 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
return new HuamiExtendedSampleProvider(device, session);
}
+ @Override
+ public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
+ return new Huami2021ActivitySummaryParser();
+ }
+
@Override
public boolean supportsAlarmSnoozing() {
// All alarms snooze by default, there doesn't seem to be a flag that disables it
@@ -194,6 +199,9 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_bt_connected_advertisement,
R.xml.devicesettings_high_mtu,
+
+ R.xml.devicesettings_header_developer,
+ R.xml.devicesettings_keep_activity_data_on_device,
};
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java
index 8d3946293..ea55d2c1e 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java
@@ -30,6 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSportsActivityType;
@@ -45,11 +46,17 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
LOG.error("Due to a bug, we can only parse the summary when startTime is already set");
return null;
}
- return parseBinaryData(summary, startTime);
+ summaryData = new JSONObject();
+ parseBinaryData(summary, startTime);
+ summary.setSummaryData(summaryData.toString());
+ return summary;
}
- private BaseActivitySummary parseBinaryData(BaseActivitySummary summary, Date startTime) {
- summaryData = new JSONObject();
+ public AbstractHuamiActivityDetailsParser getDetailsParser(final BaseActivitySummary summary) {
+ return new HuamiActivityDetailsParser(summary);
+ }
+
+ protected void parseBinaryData(BaseActivitySummary summary, Date startTime) {
ByteBuffer buffer = ByteBuffer.wrap(summary.getRawSummaryData()).order(ByteOrder.LITTLE_ENDIAN);
short version = buffer.getShort(); // version
@@ -372,13 +379,9 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
addSummaryData("swimStyle", swimStyleName);
addSummaryData("laps", laps, "laps");
}
-
- summary.setSummaryData(summaryData.toString());
- return summary;
}
-
- private void addSummaryData(String key, float value, String unit) {
+ protected void addSummaryData(String key, float value, String unit) {
if (value > 0) {
try {
JSONObject innerData = new JSONObject();
@@ -390,7 +393,7 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
}
}
- private void addSummaryData(String key, String value) {
+ protected void addSummaryData(String key, String value) {
if (key != null && !key.equals("") && value != null && !value.equals("")) {
try {
JSONObject innerData = new JSONObject();
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java
index 311737ac0..cd531deef 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java
@@ -59,6 +59,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiVibrationPatternNotificationType;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@@ -140,6 +141,11 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
return new MiBand2SampleProvider(device, session);
}
+ @Override
+ public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
+ return new HuamiActivitySummaryParser();
+ }
+
public static DateTimeDisplay getDateDisplay(Context context, String deviceAddress) throws IllegalArgumentException {
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
String dateFormatTime = context.getString(R.string.p_dateformat_time);
@@ -352,6 +358,11 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
return prefs.getBoolean("overwrite_settings_on_connection", true);
}
+ public static boolean getKeepActivityDataOnDevice(String deviceAddress) {
+ Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
+ return prefs.getBoolean("keep_activity_data_on_device", false);
+ }
+
public static VibrationProfile getVibrationProfile(String deviceAddress, HuamiVibrationPatternNotificationType notificationType) {
final String defaultVibrationProfileId;
final int defaultVibrationCount;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java
index 669ea22f4..83ad746dd 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java
@@ -115,8 +115,9 @@ public class HuamiService {
// maybe not really activity data, but steps?
public static final byte COMMAND_FETCH_DATA = 0x02;
- // maybe delete/drop activity data?
- // on Huami it's just 03 / on Huami 2021 it's 03:09
+ // delete/drop activity data
+ // on Huami it's just the single 03 byte
+ // on Huami 2021 it's followed by 09 to keep, 01 to drop from device
public static final byte COMMAND_ACK_ACTIVITY_DATA = 0x03;
public static final byte[] COMMAND_SET_FITNESS_GOAL_START = new byte[] { 0x10, 0x0, 0x0 };
@@ -230,13 +231,6 @@ public class HuamiService {
public static final byte COMMAND_FIRMWARE_CHECKSUM = 0x04; // to UUID_CHARACTERISTIC_FIRMWARE
public static final byte COMMAND_FIRMWARE_REBOOT = 0x05; // to UUID_CHARACTERISTIC_FIRMWARE
- public static final byte[] RESPONSE_FINISH_SUCCESS = new byte[] {RESPONSE, 2, SUCCESS };
- public static final byte[] RESPONSE_ACK_SUCCESS = new byte[] {RESPONSE, 3, SUCCESS };
- public static final byte[] RESPONSE_FIRMWARE_DATA_SUCCESS = new byte[] {RESPONSE, COMMAND_FIRMWARE_START_DATA, SUCCESS };
- /**
- * Received in response to any dateformat configuration request (byte 0 in the byte[] value.
- */
- public static final byte[] RESPONSE_DATEFORMAT_SUCCESS = new byte[] { RESPONSE, ENDPOINT_DISPLAY, 0x0a, 0x0, 0x01 };
public static final byte[] RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS = new byte[] { RESPONSE, COMMAND_ACTIVITY_DATA_START_DATE, SUCCESS};
public static final byte[] WEAR_LOCATION_LEFT_WRIST = new byte[] { 0x20, 0x00, 0x00, 0x02 };
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/smaq2oss/SMAQ2OSSSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/smaq2oss/SMAQ2OSSSupport.java
index 32ccc9485..7a407adf6 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/smaq2oss/SMAQ2OSSSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/smaq2oss/SMAQ2OSSSupport.java
@@ -33,7 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSuppo
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
-import nodomain.freeyourgadget.gadgetbridge.SMAQ2OSSProtos;
+import nodomain.freeyourgadget.gadgetbridge.proto.SMAQ2OSSProtos;
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummary.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummary.java
index 897696cc6..f069abd00 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummary.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummary.java
@@ -16,14 +16,9 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.model;
-import org.json.JSONObject;
-
import java.io.Serializable;
import java.util.Date;
-import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser;
-import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
-
/**
* Summarized information about a temporal activity.
*
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java
index cd10334e9..535ef81e3 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java
@@ -16,9 +16,11 @@ public class ActivitySummaryJsonSummary {
private JSONObject groupData;
private JSONObject summaryData;
private JSONObject summaryGroupedList;
+ private ActivitySummaryParser summaryParser;
private BaseActivitySummary baseActivitySummary;
- public ActivitySummaryJsonSummary(BaseActivitySummary baseActivitySummary){
+ public ActivitySummaryJsonSummary(final ActivitySummaryParser summaryParser, BaseActivitySummary baseActivitySummary){
+ this.summaryParser=summaryParser;
this.baseActivitySummary=baseActivitySummary;
}
@@ -67,8 +69,7 @@ public class ActivitySummaryJsonSummary {
private String getCorrectSummary(BaseActivitySummary item){
if (item.getRawSummaryData() != null) {
- ActivitySummaryParser parser = new HuamiActivitySummaryParser(); // FIXME: if something else than huami supports that make sure to have the right parser
- item = parser.parseBinaryData(item);
+ item = summaryParser.parseBinaryData(item);
}
return item.getSummaryData();
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/proto/.gitignore b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/proto/.gitignore
new file mode 100644
index 000000000..af230b12f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/proto/.gitignore
@@ -0,0 +1,2 @@
+# This folder will contain auto-generated protobuf classes
+*.java
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java
index fde1dae34..7ac0c10cc 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java
@@ -149,6 +149,10 @@ public class BLETypeConversions {
return (bytes[0] & 0xff) | ((bytes[1] & 0xff) << 8);
}
+ public static int toUint16(byte[] bytes, int offset) {
+ return (bytes[offset + 0] & 0xff) | ((bytes[offset + 1] & 0xff) << 8);
+ }
+
public static int toInt16(byte... bytes) {
return (short) (bytes[0] & 0xff | ((bytes[1] & 0xff) << 8));
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/AbstractHuamiActivityDetailsParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/AbstractHuamiActivityDetailsParser.java
new file mode 100644
index 000000000..0514d15e3
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/AbstractHuamiActivityDetailsParser.java
@@ -0,0 +1,48 @@
+/* Copyright (C) 2017-2021 Andreas Shimokawa, AndrewH, Carsten Pfeiffer,
+ szilardx, José Rebelo
+
+ 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.huami;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+import nodomain.freeyourgadget.gadgetbridge.GBException;
+import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
+import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
+
+public abstract class AbstractHuamiActivityDetailsParser {
+ private static final BigDecimal HUAMI_TO_DECIMAL_DEGREES_DIVISOR = new BigDecimal("3000000.0");
+
+ public abstract ActivityTrack parse(final byte[] bytes) throws GBException;
+
+ public static double convertHuamiValueToDecimalDegrees(final long huamiValue) {
+ BigDecimal result = new BigDecimal(huamiValue)
+ .divide(HUAMI_TO_DECIMAL_DEGREES_DIVISOR, GPSCoordinate.GPS_DECIMAL_DEGREES_SCALE, RoundingMode.HALF_UP);
+ return result.doubleValue();
+ }
+
+ protected static String createActivityName(final BaseActivitySummary summary) {
+ String name = summary.getName();
+ String nameText = "";
+ Long id = summary.getId();
+ if (name != null) {
+ nameText = name + " - ";
+ }
+ return nameText + id;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021ActivityDetailsParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021ActivityDetailsParser.java
new file mode 100644
index 000000000..e899fbd17
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021ActivityDetailsParser.java
@@ -0,0 +1,351 @@
+/* Copyright (C) 2022 José Rebelo
+
+ 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.huami;
+
+import androidx.annotation.Nullable;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import nodomain.freeyourgadget.gadgetbridge.GBException;
+import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
+import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
+
+public class Huami2021ActivityDetailsParser extends AbstractHuamiActivityDetailsParser {
+ private static final Logger LOG = LoggerFactory.getLogger(Huami2021ActivityDetailsParser.class);
+
+ private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
+
+ static {
+ SDF.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ private Date timestamp;
+ private long offset = 0;
+
+ private long longitude;
+ private long latitude;
+ private double altitude;
+
+ private final ActivityTrack activityTrack;
+ private ActivityPoint lastActivityPoint;
+
+ public Huami2021ActivityDetailsParser(final BaseActivitySummary summary) {
+ this.timestamp = summary.getStartTime();
+
+ this.longitude = summary.getBaseLongitude();
+ this.latitude = summary.getBaseLatitude();
+ this.altitude = summary.getBaseAltitude();
+
+ this.activityTrack = new ActivityTrack();
+ this.activityTrack.setUser(summary.getUser());
+ this.activityTrack.setDevice(summary.getDevice());
+ this.activityTrack.setName(createActivityName(summary));
+ }
+
+ @Override
+ public ActivityTrack parse(final byte[] bytes) throws GBException {
+ final ByteBuffer buf = ByteBuffer.wrap(bytes)
+ .order(ByteOrder.LITTLE_ENDIAN);
+
+ // Keep track of unknown type codes so we can print them without spamming the logs
+ final Map unknownTypeCodes = new HashMap<>();
+
+ while (buf.position() < buf.limit()) {
+ final byte typeCode = buf.get();
+ final byte length = buf.get();
+ final int initialPosition = buf.position();
+
+ final Type type = Type.fromCode(typeCode);
+ if (type == null) {
+ if (!unknownTypeCodes.containsKey(typeCode)) {
+ unknownTypeCodes.put(typeCode, 0);
+ }
+
+ unknownTypeCodes.put(typeCode, unknownTypeCodes.get(typeCode) + 1);
+ //LOG.warn("Unknown type code {} of length {}", String.format("0x%X", typeCode), length);
+ // Consume the reported length
+ buf.get(new byte[length]);
+ continue;
+ } else if (length != type.getExpectedLength()) {
+ LOG.warn("Unexpected length {} for type {}", length, type);
+ // Consume the reported length
+ buf.get(new byte[length]);
+ continue;
+ }
+
+ // Consume
+ switch (type) {
+ case TIMESTAMP:
+ consumeTimestamp(buf);
+ break;
+ case GPS_COORDS:
+ consumeGpsCoords(buf);
+ break;
+ case GPS_DELTA:
+ consumeGpsDelta(buf);
+ break;
+ case STATUS:
+ consumeStatus(buf);
+ break;
+ case SPEED:
+ consumeSpeed(buf);
+ break;
+ case ALTITUDE:
+ consumeAltitude(buf);
+ break;
+ case HEARTRATE:
+ consumeHeartRate(buf);
+ break;
+ default:
+ LOG.warn("No consumer for for type {}", type);
+ // Consume the reported length
+ buf.get(new byte[length]);
+ continue;
+ }
+
+ final int expectedPosition = initialPosition + length;
+ if (buf.position() != expectedPosition) {
+ // Should never happen unless there's a bug in one of the consumers
+ throw new IllegalStateException("Unexpected position " + buf.position() + ", expected " + expectedPosition + ", after consuming " + type);
+ }
+ }
+
+ if (!unknownTypeCodes.isEmpty()) {
+ for (final Map.Entry e : unknownTypeCodes.entrySet()) {
+ LOG.warn("Unknown type code {} seen {} times", String.format("0x%X", e.getKey()), e.getValue());
+ }
+ }
+
+ return this.activityTrack;
+ }
+
+ private void consumeTimestamp(final ByteBuffer buf) {
+ buf.getInt(); // ?
+ this.timestamp = new Date(buf.getLong());
+ this.offset = 0;
+
+ //trace("Consumed timestamp");
+ }
+
+ private void consumeTimestampOffset(final ByteBuffer buf) {
+ this.offset = buf.getShort();
+ }
+
+ private void consumeGpsCoords(final ByteBuffer buf) {
+ buf.get(new byte[6]); // ?
+ this.longitude = buf.getInt();
+ this.latitude = buf.getInt();
+ buf.get(new byte[6]); // ?
+
+ // TODO which one is the time offset? Not sure it is the first
+
+ addNewGpsCoordinates();
+
+ final double longitudeDeg = convertHuamiValueToDecimalDegrees(longitude);
+ final double latitudeDeg = convertHuamiValueToDecimalDegrees(latitude);
+
+ //trace("Consumed GPS coords: {} {}", longitudeDeg, latitudeDeg);
+ }
+
+ private void consumeGpsDelta(final ByteBuffer buf) {
+ consumeTimestampOffset(buf);
+ final short longitudeDelta = buf.getShort();
+ final short latitudeDelta = buf.getShort();
+ buf.getShort(); // ? seems to always be 2
+
+ this.longitude += longitudeDelta;
+ this.latitude += latitudeDelta;
+
+ if (lastActivityPoint == null) {
+ final String timestampStr = SDF.format(new Date(timestamp.getTime() + offset));
+ LOG.warn("{}: Got GPS delta before GPS coords, ignoring", timestampStr);
+ return;
+ }
+
+ addNewGpsCoordinates();
+
+ //trace("Consumed GPS delta: {} {}", longitudeDelta, latitudeDelta);
+ }
+
+ private void consumeStatus(final ByteBuffer buf) {
+ consumeTimestampOffset(buf);
+
+ final int statusCode = buf.getShort();
+ final String status;
+ switch (statusCode) {
+ case 1:
+ status = "start";
+ break;
+ case 4:
+ status = "pause";
+ break;
+ case 5:
+ status = "resume";
+ break;
+ case 6:
+ status = "stop";
+ break;
+ default:
+ status = String.format("unknown (0x%X)", statusCode);
+ LOG.warn("Unknown status code {}", String.format("0x%X", statusCode));
+ }
+
+ // TODO split track into multiple segments?
+
+ //trace("Consumed Status: {}", status);
+ }
+
+ private void consumeSpeed(final ByteBuffer buf) {
+ consumeTimestampOffset(buf);
+
+ final short cadence = buf.getShort(); // spm
+ final short stride = buf.getShort(); // cm
+ final short pace = buf.getShort(); // sec/km
+
+ // TODO integrate into gpx
+
+ //trace("Consumed speed: cadence={}, stride={}, ?={}", cadence, stride, );
+ }
+
+ private void consumeAltitude(final ByteBuffer buf) {
+ consumeTimestampOffset(buf);
+ altitude = (int) (buf.getInt() / 100.0f);
+
+ final ActivityPoint ap = getCurrentActivityPoint();
+ if (ap != null) {
+ final GPSCoordinate newCoordinate = new GPSCoordinate(
+ ap.getLocation().getLongitude(),
+ ap.getLocation().getLatitude(),
+ altitude
+ );
+
+ ap.setLocation(newCoordinate);
+ }
+
+ //trace("Consumed altitude: {}", altitude);
+ }
+
+ private void consumeHeartRate(final ByteBuffer buf) {
+ consumeTimestampOffset(buf);
+ final int heartRate = buf.get() & 0xff;
+
+ final ActivityPoint ap = getCurrentActivityPoint();
+ if (ap != null) {
+ ap.setHeartRate(heartRate);
+ }
+
+ //trace("Consumed HeartRate: {}", heartRate);
+ }
+
+ @Nullable
+ private ActivityPoint getCurrentActivityPoint() {
+ if (lastActivityPoint == null) {
+ return null;
+ }
+
+ // Round to the nearest second
+ final long currentTime = timestamp.getTime() + offset;
+ if (currentTime - lastActivityPoint.getTime().getTime() > 500) {
+ addNewGpsCoordinates();
+ return lastActivityPoint;
+ }
+
+ return lastActivityPoint;
+ }
+
+ private void addNewGpsCoordinates() {
+ final GPSCoordinate coordinate = new GPSCoordinate(
+ convertHuamiValueToDecimalDegrees(longitude),
+ convertHuamiValueToDecimalDegrees(latitude),
+ altitude
+ );
+
+ if (lastActivityPoint != null && lastActivityPoint.getLocation() != null && lastActivityPoint.getLocation().equals(coordinate)) {
+ // Ignore repeated location
+ return;
+ }
+
+ final ActivityPoint ap = new ActivityPoint(new Date(timestamp.getTime() + offset));
+ ap.setLocation(coordinate);
+ add(ap);
+ }
+
+ private void add(final ActivityPoint ap) {
+ if (ap == lastActivityPoint) {
+ LOG.debug("skipping point!");
+ return;
+ }
+
+ lastActivityPoint = ap;
+ activityTrack.addTrackPoint(ap);
+ }
+
+ private void trace(final String format, final Object... args) {
+ final Object[] argsWithDate = ArrayUtils.insert(0, args, SDF.format(new Date(timestamp.getTime() + offset)));
+ LOG.debug("{}: " + format, argsWithDate);
+ }
+
+ private enum Type {
+ TIMESTAMP(1, 12),
+ GPS_COORDS(2, 20),
+ GPS_DELTA(3, 8),
+ STATUS(4, 4),
+ SPEED(5, 8),
+ ALTITUDE(7, 6),
+ HEARTRATE(8, 3),
+ ;
+
+ private final byte code;
+ private final int expectedLength;
+
+ Type(final int code, final int expectedLength) {
+ this.code = (byte) code;
+ this.expectedLength = expectedLength;
+ }
+
+ public byte getCode() {
+ return this.code;
+ }
+
+ public int getExpectedLength() {
+ return this.expectedLength;
+ }
+
+ public static Type fromCode(final byte code) {
+ for (final Type type : values()) {
+ if (type.getCode() == code) {
+ return type;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021WorkoutTrackActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021WorkoutTrackActivityType.java
index c584af07b..cdd71a1eb 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021WorkoutTrackActivityType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021WorkoutTrackActivityType.java
@@ -25,22 +25,123 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
* The workout types, used to start / when workout tracking starts on the band.
*/
public enum Huami2021WorkoutTrackActivityType {
- // TODO 150 workouts :/
+ AerobicCombo(0x33),
+ Aerobics(0x6d),
+ AirWalker(0x90),
+ Archery(0x5d),
+ ArtisticSwimming(0x9c),
Badminton(0x5c),
+ Ballet(0x47),
+ BallroomDance(0x4b),
+ Baseball(0x4f),
+ Basketball(0x55),
+ BattleRope(0xa7),
+ BeachVolleyball(0x7a),
+ BellyDance(0x48),
+ Billiards(0x97),
+ bmx(0x30),
+ BoardGame(0xb1),
+ Bocce(0xaa),
+ Bowling(0x50),
+ Boxing(0x61),
+ Breaking(0xa8),
+ Bridge(0xb0),
+ CardioCombat(0x72),
+ Checkers(0xae),
+ Chess(0xad),
+ CoreTraining(0x32),
+ Cricket(0x4e),
+ CrossTraining(0x82),
+ Curling(0x29),
Dance(0x4c),
+ Darts(0x75),
+ Dodgeball(0x99),
+ DragonBoat(0x8a),
Elliptical(0x09),
+ Esports(0xbd),
+ Esquestrian(0x5e),
+ Fencing(0x94),
+ Finswimming(0x9b),
+ Fishing(0x40),
+ Flexibility(0x37),
+ Flowriding(0xac),
+ FolkDance(0x92),
Freestyle(0x05),
+ Frisbee(0x74),
+ Futsal(0xa4),
+ Gateball(0x57),
+ Gymnastics(0x3b),
+ HackySack(0xa9),
+ Handball(0x5b),
+ HIIT(0x31),
+ HipHop(0xa5),
+ HorizontalBar(0x95),
+ HulaHoop(0x73),
+ IceHockey(0x9e),
+ IceSkating(0x2c),
IndoorCycling(0x08),
IndoorFitness(0x18),
+ IndoorIceSkating(0x2d),
+ JaiAlai(0xab),
+ JazzDance(0x71),
+ Judo(0x62),
+ Jujitsu(0x93),
JumpRope(0x15),
+ Karate(0x60),
+ Kayaking(0x8c),
+ Kendo(0x5f),
+ Kickboxing(0x68),
+ KiteFlying(0x76),
+ LatinDance(0x70),
+ MartialArts(0x67),
+ MassGymnastics(0x6f),
+ ModernDance(0xb9),
+ MuayThai(0x65),
OutdoorCycling(0x04),
OutdoorRunning(0x01),
+ ParallelBars(0x96),
+ Parkour(0x81),
+ Pilates(0x3d),
+ PoleDance(0xa6),
PoolSwimming(0x06),
+ RaceWalking(0x83),
+ RockClimbing(0x46),
+ RollerSkating(0x45),
Rowing(0x17),
+ Sailing(0x41),
+ SepakTakraw(0x98),
+ Shuffleboard(0xa0),
+ Shuttlecock(0xa2),
+ Skateboarding(0x43),
+ Snorkeling(0x9d),
Soccer(0xbf),
+ Softball(0x56),
+ SomatosensoryGame(0xa3),
+ Spinning(0x8f),
+ SquareDance(0x49),
+ Squash(0x51),
+ StairClimber(0x36),
+ Stepper(0x39),
+ StreetDance(0x4a),
+ Strength(0x34),
+ Stretching(0x35),
+ Swinging(0x9f),
+ TableFootball(0xa1),
+ TableTennis(0x59),
+ TaiChi(0x64),
+ Taekwondo(0x66),
+ Tennis(0x11),
Treadmill(0x02),
+ TugOfWar(0x77),
+ Volleyball(0x58),
Walking(0x03),
+ WallBall(0x91),
+ WaterPolo(0x9a),
+ WaterRowing(0x42),
+ Weiqi(0xaf),
+ Wrestling(0x63),
Yoga(0x3c),
+ Zumba(0x4d),
;
private static final Logger LOG = LoggerFactory.getLogger(Huami2021WorkoutTrackActivityType.class);
@@ -61,6 +162,9 @@ public enum Huami2021WorkoutTrackActivityType {
return ActivityKind.TYPE_BADMINTON;
case Elliptical:
return ActivityKind.TYPE_ELLIPTICAL_TRAINER;
+ case Freestyle:
+ case IndoorFitness:
+ return ActivityKind.TYPE_EXERCISE;
case IndoorCycling:
return ActivityKind.TYPE_INDOOR_CYCLING;
case JumpRope:
@@ -78,6 +182,7 @@ public enum Huami2021WorkoutTrackActivityType {
case Treadmill:
return ActivityKind.TYPE_TREADMILL;
case Walking:
+ case RaceWalking:
return ActivityKind.TYPE_WALKING;
case Yoga:
return ActivityKind.TYPE_YOGA;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiActivityDetailsParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiActivityDetailsParser.java
index af206c294..d7d3ae11e 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiActivityDetailsParser.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiActivityDetailsParser.java
@@ -35,7 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
-public class HuamiActivityDetailsParser {
+public class HuamiActivityDetailsParser extends AbstractHuamiActivityDetailsParser {
private static final Logger LOG = LoggerFactory.getLogger(HuamiActivityDetailsParser.class);
private static final byte TYPE_GPS = 0;
@@ -47,7 +47,6 @@ public class HuamiActivityDetailsParser {
private static final byte TYPE_SPEED6 = 6;
private static final byte TYPE_SWIMMING = 8;
- private static final BigDecimal HUAMI_TO_DECIMAL_DEGREES_DIVISOR = new BigDecimal(3000000.0);
private final ActivityTrack activityTrack;
private final Date baseDate;
private long baseLongitude;
@@ -195,11 +194,6 @@ public class HuamiActivityDetailsParser {
return i;
}
- private double convertHuamiValueToDecimalDegrees(long huamiValue) {
- BigDecimal result = new BigDecimal(huamiValue).divide(HUAMI_TO_DECIMAL_DEGREES_DIVISOR, GPSCoordinate.GPS_DECIMAL_DEGREES_SCALE, RoundingMode.HALF_UP);
- return result.doubleValue();
- }
-
private int consumeHeartRate(byte[] bytes, int offset, long timeOffsetSeconds) {
int v1 = BLETypeConversions.toUint16(bytes[offset]);
int v2 = BLETypeConversions.toUint16(bytes[offset + 1]);
@@ -295,14 +289,4 @@ public class HuamiActivityDetailsParser {
LOG.debug("got packet type 8 (swimming?): " + GB.hexdump(bytes, offset, 6));
return 6;
}
-
- private String createActivityName(BaseActivitySummary summary) {
- String name = summary.getName();
- String nameText = "";
- Long id = summary.getId();
- if (name != null) {
- nameText = name + " - ";
- }
- return nameText + id;
- }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java
index 9ffef604b..18b2e1379 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java
@@ -39,6 +39,7 @@ import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
@@ -84,6 +85,7 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
}
protected void startFetching() throws IOException {
+ expectedDataLength = 0;
lastPacketCounter = -1;
TransactionBuilder builder = performInitialized(getName());
@@ -122,13 +124,28 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
}
}
+ /**
+ * Handles the finishing of fetching the activity.
+ * @param success whether fetching was successful
+ * @return whether handling the activity fetch finish was successful
+ */
@CallSuper
- protected void handleActivityFetchFinish(boolean success) {
+ protected boolean handleActivityFetchFinish(boolean success) {
GB.updateTransferNotification(null, "", false, 100, getContext());
operationFinished();
unsetBusy();
+ return true;
}
+ /**
+ * Validates that the received data has the expected checksum. Only
+ * relevant for Huami2021Support devices.
+ *
+ * @param crc32 the expected checksum
+ * @return whether the checksum was valid
+ */
+ protected abstract boolean validChecksum(int crc32);
+
/**
* Method to handle the incoming activity data.
* There are two kind of messages we currently know:
@@ -158,13 +175,18 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
if (ArrayUtils.equals(value, HuamiService.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) {
handleActivityMetadata(value);
- TransactionBuilder newBuilder = createTransactionBuilder(taskName + " Step 2");
- newBuilder.notify(characteristicActivityData, true);
- newBuilder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_FETCH_DATA});
- try {
- performImmediately(newBuilder);
- } catch (IOException ex) {
- GB.toast(getContext(), "Error fetching debug logs: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
+ if (expectedDataLength == 0 && getSupport() instanceof Huami2021Support) {
+ // Nothing to receive, if we try to fetch data it will fail
+ sendAck2021(true);
+ } else {
+ TransactionBuilder newBuilder = createTransactionBuilder(taskName + " Step 2");
+ newBuilder.notify(characteristicActivityData, true);
+ newBuilder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_FETCH_DATA});
+ try {
+ performImmediately(newBuilder);
+ } catch (IOException ex) {
+ GB.toast(getContext(), "Error fetching debug logs: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
+ }
}
return true;
} else {
@@ -177,54 +199,119 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
}
private void handleActivityMetadata(byte[] value) {
- // it's 16 on the MB7, with a 0 at the end
- if (value.length == 15 || (value.length == 16 && value[15] == 0x00)) {
- // first two bytes are whether our request was accepted
- if (ArrayUtils.equals(value, HuamiService.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) {
- // the third byte (0x01 on success) = ?
- // the 4th - 7th bytes represent the number of bytes/packets to expect, excluding the counter bytes
- expectedDataLength = BLETypeConversions.toUint32(Arrays.copyOfRange(value, 3, 7));
+ if (value.length < 3) {
+ LOG.warn("Activity metadata too short: {}", Logging.formatBytes(value));
+ handleActivityFetchFinish(false);
+ return;
+ }
- // last 8 bytes are the start date
- Calendar startTimestamp = getSupport().fromTimeBytes(Arrays.copyOfRange(value, 7, value.length));
- setStartTimestamp(startTimestamp);
+ if (value[0] != HuamiService.RESPONSE) {
+ LOG.warn("Activity metadata not a response: {}", Logging.formatBytes(value));
+ handleActivityFetchFinish(false);
+ return;
+ }
- LOG.info("Will transfer {} packets since {}", expectedDataLength, startTimestamp.getTime());
-
- GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data),
- getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since,
- DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), true, 0, getContext());
- } else {
+ switch (value[1]) {
+ case HuamiService.COMMAND_ACTIVITY_DATA_START_DATE:
+ handleStartDateResponse(value);
+ return;
+ case HuamiService.COMMAND_FETCH_DATA:
+ handleFetchDataResponse(value);
+ return;
+ case HuamiService.COMMAND_ACK_ACTIVITY_DATA:
+ // ignore, this is just the reply to the COMMAND_ACK_ACTIVITY_DATA
+ LOG.info("Got reply to COMMAND_ACK_ACTIVITY_DATA");
+ return;
+ default:
LOG.warn("Unexpected activity metadata: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
- }
- } else if (ArrayUtils.startsWith(value, HuamiService.RESPONSE_FINISH_SUCCESS)) {
- if (value.length == 3) {
- // older Huami devices, just finish
- handleActivityFetchFinish(true);
- } else if (value.length == 7 && getSupport() instanceof Huami2021Support) {
- // TODO: What do the extra 4 bytes mean?
- try {
- // not sure why we need to send this (it's acknowledging the data?) but it will get stuck otherwise
- final TransactionBuilder builder = performInitialized(getName() + " end");
- builder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_ACK_ACTIVITY_DATA, 0x09});
- builder.queue(getQueue());
- } catch (final IOException e) {
- LOG.error("Ending failed", e);
- handleActivityFetchFinish(false);
- return;
- }
- handleActivityFetchFinish(true);
- } else {
- LOG.warn("Unexpected activity metadata finish success: {}", Logging.formatBytes(value));
- handleActivityFetchFinish(false);
- }
- } else if (Arrays.equals(HuamiService.RESPONSE_ACK_SUCCESS, value) && getSupport() instanceof Huami2021Support) {
- // ignore, this is just the reply to the COMMAND_ACK_ACTIVITY_DATA
- LOG.info("Got reply to COMMAND_ACK_ACTIVITY_DATA");
- } else {
- LOG.warn("Unexpected activity metadata: {}", Logging.formatBytes(value));
+ }
+ }
+
+ private void handleStartDateResponse(final byte[] value) {
+ if (value[2] != HuamiService.SUCCESS) {
+ LOG.warn("Start date unsuccessful response: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
+ return;
+ }
+
+ // it's 16 on the MB7, with a 0 at the end
+ if (value.length != 15 && (value.length != 16 && value[15] != 0x00)) {
+ LOG.warn("Start date response length: {}", Logging.formatBytes(value));
+ handleActivityFetchFinish(false);
+ return;
+ }
+
+ // the third byte (0x01 on success) = ?
+ // the 4th - 7th bytes represent the number of bytes/packets to expect, excluding the counter bytes
+ expectedDataLength = BLETypeConversions.toUint32(Arrays.copyOfRange(value, 3, 7));
+
+ // last 8 bytes are the start date
+ Calendar startTimestamp = getSupport().fromTimeBytes(Arrays.copyOfRange(value, 7, value.length));
+
+ if (expectedDataLength == 0) {
+ LOG.info("No data to fetch since {}", startTimestamp.getTime());
+ handleActivityFetchFinish(true);
+ return;
+ }
+
+ setStartTimestamp(startTimestamp);
+ LOG.info("Will transfer {} packets since {}", expectedDataLength, startTimestamp.getTime());
+
+ GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data),
+ getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since,
+ DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), true, 0, getContext());
+ }
+
+ private void handleFetchDataResponse(final byte[] value) {
+ if (value[2] != HuamiService.SUCCESS) {
+ LOG.warn("Fetch data unsuccessful response: {}", Logging.formatBytes(value));
+ handleActivityFetchFinish(false);
+ return;
+ }
+
+ if (value.length != 3 && value.length != 7) {
+ LOG.warn("Fetch data unexpected metadata length: {}", Logging.formatBytes(value));
+ handleActivityFetchFinish(false);
+ return;
+ }
+
+ if (value.length == 7 && !validChecksum(BLETypeConversions.toUint32(value, 3))) {
+ LOG.warn("Data checksum invalid");
+ handleActivityFetchFinish(false);
+ sendAck2021(true);
+ return;
+ }
+
+ boolean handleFinishSuccess;
+ try {
+ handleFinishSuccess = handleActivityFetchFinish(true);
+ } catch (final Exception e) {
+ LOG.warn("Failed to handle activity fetch finish", e);
+ handleFinishSuccess = false;
+ }
+
+ final boolean keepActivityDataOnDevice = HuamiCoordinator.getKeepActivityDataOnDevice(getDevice().getAddress());
+
+ sendAck2021(keepActivityDataOnDevice || !handleFinishSuccess);
+ }
+
+ private void sendAck2021(final boolean keepDataOnDevice) {
+ if (!(getSupport() instanceof Huami2021Support)) {
+ return;
+ }
+
+ // 0x01 to ACK, mark as saved on phone (drop from band)
+ // 0x09 to ACK, but keep it marked as not saved
+ // If 0x01 is sent, detailed information seems to be discarded, and is not sent again anymore
+ final byte ackByte = (byte) (keepDataOnDevice ? 0x09 : 0x01);
+
+ try {
+ final TransactionBuilder builder = performInitialized(getName() + " end");
+ builder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_ACK_ACTIVITY_DATA, ackByte});
+ performImmediately(builder);
+ } catch (final IOException e) {
+ LOG.error("Ending failed", e);
}
}
@@ -242,7 +329,6 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
editor.apply();
}
-
protected GregorianCalendar getLastSuccessfulSyncTime() {
long timeStampMillis = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getLong(getLastSyncTimeKey(), 0);
if (timeStampMillis != 0) {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchActivityOperation.java
index 5436676d4..981821936 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchActivityOperation.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchActivityOperation.java
@@ -45,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
+import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -78,21 +79,31 @@ public class FetchActivityOperation extends AbstractFetchOperation {
startFetching(builder, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_ACTIVTY, sinceWhen);
}
- protected void handleActivityFetchFinish(boolean success) {
+ @Override
+ protected boolean handleActivityFetchFinish(boolean success) {
LOG.info("{} has finished round {}", getName(), fetchCount);
GregorianCalendar lastSyncTimestamp = saveSamples();
if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) {
try {
startFetching();
- return;
+ return true;
} catch (IOException ex) {
LOG.error("Error starting another round of {}", getName(), ex);
+ return false;
}
}
- super.handleActivityFetchFinish(success);
+ final boolean superSuccess = super.handleActivityFetchFinish(success);
GB.signalActivityDataFinish();
+ return superSuccess;
+ }
+
+ @Override
+ protected boolean validChecksum(int crc32) {
+ // TODO actually check it
+ LOG.warn("Checksum not implemented for activity data, assuming it's valid");
+ return true;
}
private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsDetailsOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsDetailsOperation.java
index 3e5313c5b..c681cde8f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsDetailsOperation.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsDetailsOperation.java
@@ -24,6 +24,8 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
import java.util.GregorianCalendar;
import androidx.annotation.NonNull;
@@ -39,8 +41,10 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
+import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -51,15 +55,20 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
*/
public class FetchSportsDetailsOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchSportsDetailsOperation.class);
+ private final AbstractHuamiActivityDetailsParser detailsParser;
private final BaseActivitySummary summary;
private final String lastSyncTimeKey;
private ByteArrayOutputStream buffer;
- FetchSportsDetailsOperation(@NonNull BaseActivitySummary summary, @NonNull HuamiSupport support, @NonNull String lastSyncTimeKey) {
+ FetchSportsDetailsOperation(@NonNull BaseActivitySummary summary,
+ @NonNull AbstractHuamiActivityDetailsParser detailsParser,
+ @NonNull HuamiSupport support,
+ @NonNull String lastSyncTimeKey) {
super(support);
setName("fetching sport details");
this.summary = summary;
+ this.detailsParser = detailsParser;
this.lastSyncTimeKey = lastSyncTimeKey;
}
@@ -72,7 +81,7 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
}
@Override
- protected void handleActivityFetchFinish(boolean success) {
+ protected boolean handleActivityFetchFinish(boolean success) {
LOG.info(getName() + " has finished round " + fetchCount);
// GregorianCalendar lastSyncTimestamp = saveSamples();
// if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) {
@@ -84,12 +93,14 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
// }
// }
+ boolean parseSuccess = true;
- if (success) {
- HuamiActivityDetailsParser parser = new HuamiActivityDetailsParser(summary);
- parser.setSkipCounterByte(false); // is already stripped
+ if (success && buffer.size() > 0) {
+ if (detailsParser instanceof HuamiActivityDetailsParser) {
+ ((HuamiActivityDetailsParser) detailsParser).setSkipCounterByte(false); // is already stripped
+ }
try {
- ActivityTrack track = parser.parse(buffer.toByteArray());
+ ActivityTrack track = detailsParser.parse(buffer.toByteArray());
ActivityTrackExporter exporter = createExporter();
String trackType = "track";
switch (summary.getActivityKind()) {
@@ -112,6 +123,8 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
trackType = getContext().getString(R.string.activity_type_swimming);
break;
}
+ final String rawBytesPath = saveRawBytes();
+
String fileName = FileUtils.makeValidFileName("gadgetbridge-"+trackType.toLowerCase()+"-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx");
File targetFile = new File(FileUtils.getExternalFilesDir(), fileName);
@@ -120,21 +133,35 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
summary.setGpxTrack(targetFile.getAbsolutePath());
+ if (rawBytesPath != null) {
+ summary.setRawDetailsPath(rawBytesPath);
+ }
dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary);
}
} catch (ActivityTrackExporter.GPXTrackEmptyException ex) {
GB.toast(getContext(), "This activity does not contain GPX tracks.", Toast.LENGTH_LONG, GB.ERROR, ex);
}
-
- GregorianCalendar endTime = BLETypeConversions.createCalendar();
- endTime.setTime(summary.getEndTime());
- saveLastSyncTimestamp(endTime);
} catch (Exception ex) {
GB.toast(getContext(), "Error getting activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
+ parseSuccess = false;
}
}
- super.handleActivityFetchFinish(success);
+ if (success && parseSuccess) {
+ // Always increment the sync timestamp on success, even if we did not get data
+ GregorianCalendar endTime = BLETypeConversions.createCalendar();
+ endTime.setTime(summary.getEndTime());
+ saveLastSyncTimestamp(endTime);
+ }
+
+ final boolean superSuccess = super.handleActivityFetchFinish(success);
+
+ return superSuccess && parseSuccess;
+ }
+
+ @Override
+ protected boolean validChecksum(int crc32) {
+ return crc32 == CheckSums.getCRC32(buffer.toByteArray());
}
private ActivityTrackExporter createExporter() {
@@ -198,4 +225,23 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
calendar.setTime(summary.getStartTime());
return calendar;
}
+
+ private String saveRawBytes() {
+ final String fileName = FileUtils.makeValidFileName(String.format("%s.bin", DateTimeUtils.formatIso8601(summary.getStartTime())));
+ FileOutputStream outputStream = null;
+
+ try {
+ final File targetFolder = new File(FileUtils.getExternalFilesDir(), "rawDetails");
+ targetFolder.mkdirs();
+ final File targetFile = new File(targetFolder, fileName);
+ outputStream = new FileOutputStream(targetFile);
+ outputStream.write(buffer.toByteArray());
+ outputStream.close();
+ return targetFile.getAbsolutePath();
+ } catch (final IOException e) {
+ LOG.error("Failed to save raw bytes", e);
+ }
+
+ return null;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsSummaryOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsSummaryOperation.java
index 531312423..b22f01dce 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsSummaryOperation.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsSummaryOperation.java
@@ -32,14 +32,21 @@ 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.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021ActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
+import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@@ -63,7 +70,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
}
@Override
- protected void handleActivityFetchFinish(boolean success) {
+ protected boolean handleActivityFetchFinish(boolean success) {
LOG.info(getName() + " has finished round " + fetchCount);
// GregorianCalendar lastSyncTimestamp = saveSamples();
@@ -77,12 +84,16 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
// }
BaseActivitySummary summary = null;
- if (success) {
+ final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(getDevice());
+ final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(getDevice());
+
+ boolean parseSummarySuccess = true;
+
+ if (success && buffer.size() > 0) {
summary = new BaseActivitySummary();
summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set
summary.setRawSummaryData(buffer.toByteArray());
- HuamiActivitySummaryParser parser = new HuamiActivitySummaryParser();
- summary = parser.parseBinaryData(summary);
+ summary = summaryParser.parseBinaryData(summary);
if (summary != null) {
summary.setSummaryData(null); // remove json before saving to database,
try (DBHandler dbHandler = GBApplication.acquireDB()) {
@@ -95,21 +106,32 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
session.getBaseActivitySummaryDao().insertOrReplace(summary);
} catch (Exception ex) {
GB.toast(getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex);
+ parseSummarySuccess = false;
}
}
}
- super.handleActivityFetchFinish(success);
+ final boolean superSuccess = super.handleActivityFetchFinish(success);
+ boolean getDetailsSuccess = true;
if (summary != null) {
- FetchSportsDetailsOperation nextOperation = new FetchSportsDetailsOperation(summary, getSupport(), getLastSyncTimeKey());
+ final AbstractHuamiActivityDetailsParser detailsParser = ((HuamiActivitySummaryParser) summaryParser).getDetailsParser(summary);
+
+ FetchSportsDetailsOperation nextOperation = new FetchSportsDetailsOperation(summary, detailsParser, getSupport(), getLastSyncTimeKey());
try {
nextOperation.perform();
} catch (IOException ex) {
GB.toast(getContext(), "Unable to fetch activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
+ getDetailsSuccess = false;
}
}
+ return parseSummarySuccess && superSuccess && getDetailsSuccess;
+ }
+
+ @Override
+ protected boolean validChecksum(int crc32) {
+ return crc32 == CheckSums.getCRC32(buffer.toByteArray());
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/HuamiFetchDebugLogsOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/HuamiFetchDebugLogsOperation.java
index a17b3c840..edbf915c4 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/HuamiFetchDebugLogsOperation.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/HuamiFetchDebugLogsOperation.java
@@ -36,6 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipS
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
+import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -80,16 +81,24 @@ public class HuamiFetchDebugLogsOperation extends AbstractFetchOperation {
}
@Override
- protected void handleActivityFetchFinish(boolean success) {
- LOG.info(getName() +" data has finished");
+ protected boolean handleActivityFetchFinish(boolean success) {
+ LOG.info("{} data has finished", getName());
try {
logOutputStream.close();
logOutputStream = null;
} catch (IOException e) {
LOG.warn("could not close output stream", e);
- return;
+ return false;
}
- super.handleActivityFetchFinish(success);
+
+ return super.handleActivityFetchFinish(success);
+ }
+
+ @Override
+ protected boolean validChecksum(int crc32) {
+ // TODO actually check it?
+ LOG.warn("Checksum not implemented for debug logs, assuming it's valid");
+ return true;
}
@Override
diff --git a/app/src/main/proto/huami.proto b/app/src/main/proto/huami.proto
new file mode 100644
index 000000000..d70f9f764
--- /dev/null
+++ b/app/src/main/proto/huami.proto
@@ -0,0 +1,80 @@
+syntax = "proto3";
+
+option java_package = "nodomain.freeyourgadget.gadgetbridge.proto";
+option java_outer_classname = "HuamiProtos";
+
+message WorkoutSummary {
+ string version = 1;
+ Location location = 2;
+ Type type = 3;
+ Distance distance = 4;
+ Steps steps = 11;
+ Time time = 7;
+ Pace pace = 10;
+ HeartRate heartRate = 19;
+ Calories calories = 16;
+ TrainingEffect trainingEffect = 21;
+ HeartRateZones heartRateZones = 22;
+}
+
+message Location {
+ // TODO 1, 2, 3
+ int32 baseLatitude = 5; // /6000000 -> coords
+ int32 baseLongitude = 6; // /-6000000 -> coords
+ int32 baseAltitude = 7; // /2 -> meters
+ int32 maxLatitude = 8; // /3000000 -> coords
+ int32 minLatitude = 9; // /3000000 -> coords
+ int32 maxLongitude = 10; // /3000000 -> coords
+ int32 minLongitude = 11; // /3000000 -> coords
+}
+
+message HeartRate {
+ int32 avg = 1; // bpm
+ int32 max = 2; // bpm
+ int32 min = 3; // bpm
+}
+
+message Steps {
+ float avgCadence = 1; // steps/sec
+ float maxCadence = 2; // steps/sec
+ int32 avgStride = 3; // cm
+ int32 steps = 4; // count
+}
+
+message Type {
+ int32 type = 1; // 1 = running, 4 = bike, 3 = walk
+ // TODO 2, always 0?
+}
+
+message Distance {
+ float distance = 1; // meters
+}
+
+message Time {
+ int32 totalDuration = 1; // seconds
+ int32 workoutDuration = 2; // seconds
+ int32 pauseDuration = 3; // seconds
+}
+
+message Pace {
+ float avg = 1; // val * 1000 / 60 -> min/km
+ float best = 2; // val * 1000 / 60 -> min/km
+}
+
+message Calories {
+ int32 calories = 1; // kcal
+}
+
+message HeartRateZones {
+ // TODO 1, is always = 1?
+ // Zones: N/A, Warm-up, Fat-burn time, Aerobic, Anaerobic, Extreme
+ repeated int32 zoneMax = 2; // bpm
+ repeated int32 zoneTime = 3; // seconds
+}
+
+message TrainingEffect {
+ float aerobicTrainingEffect = 4;
+ float anaerobicTrainingEffect = 5;
+ int32 currentWorkoutLoad = 6;
+ int32 maximumOxygenUptake = 7; // ml/kg/min
+}
diff --git a/app/src/main/proto/smaq2oss.proto b/app/src/main/proto/smaq2oss.proto
index 2e0de5205..aca887950 100644
--- a/app/src/main/proto/smaq2oss.proto
+++ b/app/src/main/proto/smaq2oss.proto
@@ -1,6 +1,6 @@
syntax = "proto3";
-option java_package = "nodomain.freeyourgadget.gadgetbridge";
+option java_package = "nodomain.freeyourgadget.gadgetbridge.proto";
option java_outer_classname = "SMAQ2OSSProtos";
message SetTime
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 651612f64..5134e5c76 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -246,6 +246,7 @@
Call Dismissal
Update on device
Developer options
+ Authentication
Mi Band address
Pebble settings
Activity trackers
@@ -723,7 +724,7 @@
Average: %1$s
Do not ACK activity data transfer
If not ACKed to the band, activity data is not cleared. Useful if GB is used together with other apps.
- Will keep activity data on the Mi Band even after synchronization. Useful if GB is used together with other apps.
+ Will keep activity data on the device even after synchronization. Useful if GB is used together with other apps.
Use low-latency mode for firmware flashing
This might help on devices where firmware flashing fails.
Allow 3rd party apps to change settings
@@ -1459,6 +1460,8 @@
7 days
30 days
Time period
+ Delete %d activities
+ Are you sure you want to delete %d activities?
All devices
distant past
today
diff --git a/app/src/main/res/xml/devicesettings_header_authentication.xml b/app/src/main/res/xml/devicesettings_header_authentication.xml
new file mode 100644
index 000000000..9050be3e9
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_header_authentication.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/app/src/main/res/xml/devicesettings_header_developer.xml b/app/src/main/res/xml/devicesettings_header_developer.xml
new file mode 100644
index 000000000..961e98776
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_header_developer.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/app/src/main/res/xml/devicesettings_keep_activity_data_on_device.xml b/app/src/main/res/xml/devicesettings_keep_activity_data_on_device.xml
new file mode 100644
index 000000000..c2809866e
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_keep_activity_data_on_device.xml
@@ -0,0 +1,9 @@
+
+
+
+