diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiEncryptedSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiEncryptedSupport.java
index 008c72cac..87c17cd91 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiEncryptedSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiEncryptedSupport.java
@@ -80,6 +80,7 @@ public class XiaomiEncryptedSupport extends XiaomiSupport {
this.characteristicCommandWrite = new XiaomiCharacteristic(this, btCharacteristicCommandWrite, authService);
this.characteristicCommandRead.setEncrypted(true);
this.characteristicActivityData = new XiaomiCharacteristic(this, btCharacteristicActivityData, authService);
+ this.characteristicActivityData.setHandler(healthService.getActivityFetcher()::addChunk);
this.characteristicCommandRead.setEncrypted(true);
this.characteristicDataUpload = new XiaomiCharacteristic(this, btCharacteristicDataUpload, authService);
this.characteristicCommandRead.setEncrypted(true);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java
new file mode 100644
index 000000000..f996b4842
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java
@@ -0,0 +1,145 @@
+/* Copyright (C) 2023 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.xiaomi.activity;
+
+import android.content.Context;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiHealthService;
+import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class XiaomiActivityFileFetcher {
+ private static final Logger LOG = LoggerFactory.getLogger(XiaomiActivityFileFetcher.class);
+
+ private final XiaomiHealthService mHealthService;
+
+ private final Queue> mFetchQueue = new LinkedList<>();
+ private ByteArrayOutputStream mBuffer = null;
+ private Set pendingFiles = new HashSet<>();
+ private boolean isFetching = false;
+
+ public XiaomiActivityFileFetcher(final XiaomiHealthService healthService) {
+ this.mHealthService = healthService;
+ }
+
+ public void addChunk(final byte[] chunk) {
+ final int total = BLETypeConversions.toUint16(chunk, 0);
+ final int num = BLETypeConversions.toUint16(chunk, 2);
+
+ LOG.debug("Got activity chunk {}/{}", num, total);
+
+ if (num == 1) {
+ if (mBuffer == null) {
+ mBuffer = new ByteArrayOutputStream();
+ }
+ mBuffer.reset();
+ mBuffer.write(chunk, 4, chunk.length - 4);
+ }
+
+ if (num == total) {
+ final byte[] data = mBuffer.toByteArray();
+ mBuffer.reset();
+ mBuffer = null;
+
+ if (data.length < 13) {
+ LOG.warn("Activity data length of {} is too short", data.length);
+ // FIXME this may mess up the order.. maybe we should just abort
+ triggerNextFetch();
+ return;
+ }
+
+ if (!validChecksum(data)) {
+ LOG.warn("Invalid activity data checksum");
+ // FIXME this may mess up the order.. maybe we should just abort
+ triggerNextFetch();
+ return;
+ }
+
+ if (data[7] != 0) {
+ LOG.warn(
+ "Unexpected activity payload byte {} at position 7 - parsing might fail",
+ String.format("0x%02X", data[7])
+ );
+ }
+
+ final byte[] fileIdBytes = Arrays.copyOfRange(data, 0, 7);
+ final byte[] activityData = Arrays.copyOfRange(data, 8, data.length - 4);
+ final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(fileIdBytes);
+
+ final XiaomiActivityParser activityParser = XiaomiActivityParser.create(fileId);
+ if (activityParser == null) {
+ LOG.warn("Failed to find activity parser for {}", fileId);
+ triggerNextFetch();
+ return;
+ }
+
+ if (activityParser.parse(fileId, activityData)) {
+ LOG.debug("Acking recorded data {}", fileId);
+ //mHealthService.ackRecordedData(fileId);
+ }
+
+ // FIXME only after receiving everything triggerNextFetch();
+ }
+ }
+
+ public void fetch(final List fileIds) {
+ mFetchQueue.add(fileIds);
+ if (!isFetching) {
+ // Currently not fetching anything, fetch the next
+ final XiaomiSupport support = mHealthService.getSupport();
+ final Context context = support.getContext();
+ GB.updateTransferNotification(context.getString(R.string.busy_task_fetch_activity_data),"", true, 0, context);
+ support.getDevice().setBusyTask(context.getString(R.string.busy_task_fetch_activity_data));
+ triggerNextFetch();
+ }
+ }
+
+ private void triggerNextFetch() {
+ final List fileIds = mFetchQueue.poll();
+
+ if (fileIds == null || fileIds.isEmpty()) {
+ mHealthService.getSupport().getDevice().unsetBusyTask();
+ GB.updateTransferNotification(null, "", false, 100, mHealthService.getSupport().getContext());
+ return;
+ }
+
+ mHealthService.requestRecordedData(fileIds);
+ }
+
+ public boolean validChecksum(final byte[] arr) {
+ final int arrCrc32 = CheckSums.getCRC32(arr, 0, arr.length - 4);
+ final int expectedCrc32 = BLETypeConversions.toUint32(arr, arr.length - 4);
+
+ return arrCrc32 == expectedCrc32;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java
new file mode 100644
index 000000000..18321edda
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java
@@ -0,0 +1,125 @@
+/* Copyright (C) 2023 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.xiaomi.activity;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Date;
+
+import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
+
+public class XiaomiActivityFileId {
+ public static final int TYPE_ACTIVITY = 0;
+ public static final int TYPE_SPORTS = 1;
+
+ public static final int TYPE_DETAILS = 0;
+ public static final int TYPE_SUMMARY = 1;
+
+ private final Date timestamp;
+ private final int timezone;
+ private final int type;
+ private final int subtype;
+ private final int detailType;
+ private final int version;
+
+ public XiaomiActivityFileId(final Date timestamp,
+ final int timezone,
+ final int type,
+ final int subtype,
+ final int detailType,
+ final int version) {
+ this.timestamp = timestamp;
+ this.timezone = timezone;
+ this.type = type;
+ this.subtype = subtype;
+ this.detailType = detailType;
+ this.version = version;
+ }
+
+ public Date getTimestamp() {
+ return timestamp;
+ }
+
+ public int getTimezone() {
+ return timezone;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ public int getSubtype() {
+ return subtype;
+ }
+
+ public int getDetailType() {
+ return detailType;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+ public byte[] toBytes() {
+ final ByteBuffer buf = ByteBuffer.allocate(7).order(ByteOrder.LITTLE_ENDIAN);
+
+ buf.putInt((int) (timestamp.getTime() / 1000));
+ buf.put((byte) timezone);
+ buf.put((byte) version);
+ buf.put((byte) (type << 7 | subtype << 2 | detailType));
+
+ return buf.array();
+ }
+
+ public static XiaomiActivityFileId from(final byte[] bytes) {
+ assert bytes.length == 7;
+
+ return from(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN));
+ }
+
+ public static XiaomiActivityFileId from(final ByteBuffer buf) {
+ final int ts = buf.getInt();
+ final int tz = buf.get(); // 15 min blocks
+ final int version = buf.get();
+ final int flags = buf.get();
+ // bit 0 is type - 0 activity, 1 sports
+ final int type = (flags >> 7) & 1;
+ // bit 1 to 6 bits are subtype
+ // for activity: activity, sleep, measurements, etc
+ // for workout: workout type (8 freestyle)
+ final int subtype = (flags & 127) >> 2;
+ // bit 6 and 7 - 0 details, 1 summary
+ final int detailType = flags & 3;
+
+ return new XiaomiActivityFileId(new Date(ts * 1000L), tz, version, type, subtype, detailType);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{" +
+ "timestamp=" + DateTimeUtils.formatIso8601(timestamp) +
+ ", timezone=" + timezone +
+ ", type=" + type +
+ ", subtype=" + subtype +
+ ", detailType=" + detailType +
+ ", version=" + version +
+ "}";
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java
index 026172577..f1d5892d0 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java
@@ -16,6 +16,50 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity;
-public class XiaomiActivityParser {
+import androidx.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
+
+public abstract class XiaomiActivityParser {
+ private static final Logger LOG = LoggerFactory.getLogger(XiaomiActivityParser.class);
+
+ private final XiaomiSupport mSupport;
+
+ public XiaomiActivityParser(final XiaomiSupport support) {
+ this.mSupport = support;
+ }
+
+ public abstract boolean parse(final XiaomiActivityFileId fileId, final byte[] bytes);
+
+ public XiaomiSupport getSupport() {
+ return mSupport;
+ }
+
+ @Nullable
+ public static XiaomiActivityParser create(final XiaomiActivityFileId fileId) {
+ switch (fileId.getType()) {
+ case XiaomiActivityFileId.TYPE_ACTIVITY:
+ return createForActivity(fileId);
+ case XiaomiActivityFileId.TYPE_SPORTS:
+ return createForSports(fileId);
+ }
+
+ LOG.warn("Unknown file type for {}", fileId);
+ return null;
+ }
+
+ public static XiaomiActivityParser createForActivity(final XiaomiActivityFileId fileId) {
+ assert fileId.getType() == XiaomiActivityFileId.TYPE_ACTIVITY;
+
+ return null;
+ }
+
+ public static XiaomiActivityParser createForSports(final XiaomiActivityFileId fileId) {
+ assert fileId.getType() == XiaomiActivityFileId.TYPE_SPORTS;
+
+ return null;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/AbstractXiaomiService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/AbstractXiaomiService.java
index 3a1d73b3a..18b2be35f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/AbstractXiaomiService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/AbstractXiaomiService.java
@@ -52,7 +52,7 @@ public abstract class AbstractXiaomiService {
return false;
}
- protected XiaomiSupport getSupport() {
+ public XiaomiSupport getSupport() {
return mSupport;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java
index 45aa6e760..ebc5b4fef 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java
@@ -20,14 +20,18 @@ import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import com.google.protobuf.ByteString;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
+import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@@ -49,6 +53,8 @@ import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiPreferences;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileFetcher;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileId;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@@ -58,8 +64,10 @@ public class XiaomiHealthService extends AbstractXiaomiService {
public static final int COMMAND_TYPE = 8;
private static final int CMD_SET_USER_INFO = 0;
- private static final int CMD_ACTIVITY_FETCH_1 = 1;
- private static final int CMD_ACTIVITY_FETCH_2 = 2;
+ private static final int CMD_ACTIVITY_FETCH_TODAY = 1;
+ private static final int CMD_ACTIVITY_FETCH_PAST = 2;
+ private static final int CMD_ACTIVITY_FETCH_REQUEST = 3;
+ private static final int CMD_ACTIVITY_FETCH_ACK = 5;
private static final int CMD_CONFIG_SPO2_GET = 8;
private static final int CMD_CONFIG_SPO2_SET = 9;
private static final int CMD_CONFIG_HEART_RATE_GET = 10;
@@ -79,6 +87,8 @@ public class XiaomiHealthService extends AbstractXiaomiService {
private boolean realtimeOneShot = false;
private int previousSteps = -1;
+ private final XiaomiActivityFileFetcher activityFetcher = new XiaomiActivityFileFetcher(this);
+
public XiaomiHealthService(final XiaomiSupport support) {
super(support);
}
@@ -89,9 +99,9 @@ public class XiaomiHealthService extends AbstractXiaomiService {
case CMD_SET_USER_INFO:
LOG.debug("Got user info set ack, status={}", cmd.getStatus());
return;
- case CMD_ACTIVITY_FETCH_1:
- case CMD_ACTIVITY_FETCH_2:
- handleActivityFetchResponse(cmd.getHealth().getActivityRecordIds().toByteArray());
+ case CMD_ACTIVITY_FETCH_TODAY:
+ case CMD_ACTIVITY_FETCH_PAST:
+ handleActivityFetchResponse(cmd.getSubtype(), cmd.getHealth().getActivityRequestFileIds().toByteArray());
return;
case CMD_CONFIG_SPO2_GET:
handleSpo2Config(cmd.getHealth().getSpo2());
@@ -395,62 +405,97 @@ public class XiaomiHealthService extends AbstractXiaomiService {
);
}
+ public XiaomiActivityFileFetcher getActivityFetcher() {
+ return activityFetcher;
+ }
+
public void onFetchRecordedData(final int dataTypes) {
LOG.debug("Fetch recorded data: {}", dataTypes);
- final TransactionBuilder builder = getSupport().createTransactionBuilder("fetch recorded data step 1");
+ fetchRecordedDataToday();
+ }
+
+ private void fetchRecordedDataToday() {
getSupport().sendCommand(
- builder,
+ "fetch recorded data today",
XiaomiProto.Command.newBuilder()
.setType(COMMAND_TYPE)
- .setSubtype(CMD_ACTIVITY_FETCH_1)
- .setHealth(XiaomiProto.Health.newBuilder().setActivitySyncRequest1(
- // TODO official app sends 0, but doesn't work every time?
- XiaomiProto.ActivitySyncRequest1.newBuilder().setUnknown1(1)
+ .setSubtype(CMD_ACTIVITY_FETCH_TODAY)
+ .setHealth(XiaomiProto.Health.newBuilder().setActivitySyncRequestToday(
+ // TODO official app sends 0, but sometimes 1?
+ XiaomiProto.ActivitySyncRequestToday.newBuilder().setUnknown1(0)
))
.build()
);
-
- // TODO we need to wait for the reply from the previous before sending this one?
- //getSupport().sendCommand(
- // builder,
- // XiaomiProto.Command.newBuilder()
- // .setType(COMMAND_TYPE)
- // .setSubtype(CMD_ACTIVITY_FETCH_2)
- // .build()
- //);
-
- builder.queue(getSupport().getQueue());
}
- public void handleActivityFetchResponse(final byte[] recordIds) {
+ private void fetchRecordedDataPast() {
+ getSupport().sendCommand(
+ "fetch recorded data past",
+ XiaomiProto.Command.newBuilder()
+ .setType(COMMAND_TYPE)
+ .setSubtype(CMD_ACTIVITY_FETCH_PAST)
+ .build()
+ );
+ }
+
+ public void requestRecordedData(final List fileIds) {
+ final ByteBuffer buf = ByteBuffer.allocate(7 * fileIds.size()).order(ByteOrder.LITTLE_ENDIAN);
+ for (final XiaomiActivityFileId fileId : fileIds) {
+ buf.put(fileId.toBytes());
+ }
+
+ getSupport().sendCommand(
+ "request recorded data",
+ XiaomiProto.Command.newBuilder()
+ .setType(COMMAND_TYPE)
+ .setSubtype(CMD_ACTIVITY_FETCH_REQUEST)
+ .setHealth(XiaomiProto.Health.newBuilder().setActivityRequestFileIds(
+ ByteString.copyFrom(buf.array())
+ ))
+ .build()
+ );
+ }
+
+ public void ackRecordedData(final XiaomiActivityFileId fileId) {
+ getSupport().sendCommand(
+ "ack recorded data",
+ XiaomiProto.Command.newBuilder()
+ .setType(COMMAND_TYPE)
+ .setSubtype(CMD_ACTIVITY_FETCH_ACK)
+ .setHealth(XiaomiProto.Health.newBuilder().setActivitySyncAckFileIds(
+ ByteString.copyFrom(fileId.toBytes())
+ ))
+ .build()
+ );
+ }
+
+ public void handleActivityFetchResponse(final int subtype, final byte[] recordIds) {
if ((recordIds.length % 7) != 0) {
LOG.warn("recordIds {} length = {}, not a multiple of 7, can't parse", GB.hexdump(recordIds), recordIds.length);
return;
-
}
LOG.debug("Got {} record IDs", recordIds.length / 7);
final ByteBuffer buf = ByteBuffer.wrap(recordIds).order(ByteOrder.LITTLE_ENDIAN);
+ final List fileIds = new ArrayList<>();
+
while (buf.position() < buf.limit()) {
- final int ts = buf.getInt();
- final int tz = buf.get(); // 15 min blocks
- final int version = buf.get();
- final int flags = buf.get();
- // bit 0 is type - 0 activity, 1 sports
- final int type = (flags >> 7) & 1;
- // bit 1 to 6 bits are subtype
- // for activity: activity, sleep, measurements, etc
- // for workout: workout type (8 freestyle)
- final int subtype = (flags & 127) >> 2;
- // bit 6 and 7 - 0/1 - summary vs details
- final int detailType = flags & 3;
- LOG.debug(
- "Activity Record: ts = {}, tz = {}, flags = {}, type = {}, subtype = {}, detailType = {}, version = {}",
- ts, tz, String.format("0x%02X", flags), type, subtype, detailType, version
- );
+ final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(buf);
+ LOG.debug("Got activity to fetch: {}", fileId);
+ fileIds.add(fileId);
+ }
+
+ if (!fileIds.isEmpty()) {
+ LOG.debug("Fetching {} files", fileIds.size());
+ activityFetcher.fetch(fileIds);
+ }
+
+ if (subtype == CMD_ACTIVITY_FETCH_TODAY) {
+ LOG.debug("Fetch recorded data from the past");
+ // FIXME fix scheduling fetchRecordedDataPast();
}
}
diff --git a/app/src/main/proto/xiaomi.proto b/app/src/main/proto/xiaomi.proto
index 8e87d23bb..4daaa0f2c 100644
--- a/app/src/main/proto/xiaomi.proto
+++ b/app/src/main/proto/xiaomi.proto
@@ -282,8 +282,11 @@ message Charger {
message Health {
optional UserInfo userInfo = 1;
- optional bytes activityRecordIds = 2;
- optional ActivitySyncRequest1 activitySyncRequest1 = 5;
+ // 8, 2 get today | 8, 3 get past
+ optional bytes activityRequestFileIds = 2;
+ //
+ optional bytes activitySyncAckFileIds = 3;
+ optional ActivitySyncRequestToday activitySyncRequestToday = 5;
optional SpO2 spo2 = 7;
optional HeartRate heartRate = 8;
@@ -321,8 +324,8 @@ message UserInfo {
optional uint32 goalMoving = 11; // minutes
}
-message ActivitySyncRequest1 {
- optional uint32 unknown1 = 1; // 0/1
+message ActivitySyncRequestToday {
+ optional uint32 unknown1 = 1; // 0 most of the time, sometimes 1
}
message SpO2 {
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileIdTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileIdTest.java
new file mode 100644
index 000000000..ca8d7e121
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileIdTest.java
@@ -0,0 +1,55 @@
+/* Copyright (C) 2023 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.xiaomi.activity;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+import java.util.Date;
+
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class XiaomiActivityFileIdTest {
+ @Test
+ public void testEncode() {
+ final byte[] bytes = GB.hexStringToByteArray("21F328650403A0");
+ final XiaomiActivityFileId xiaomiActivityFileId = new XiaomiActivityFileId(
+ new Date(1697182497000L),
+ 4,
+ 3,
+ 1,
+ 8,
+ 0
+ );
+
+ assertArrayEquals(bytes, xiaomiActivityFileId.toBytes());
+ }
+
+ @Test
+ public void testDecode() {
+ final byte[] bytes = GB.hexStringToByteArray("21F328650403A0");
+ final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(bytes);
+
+ assertEquals(1697182497000L, fileId.getTimestamp().getTime());
+ assertEquals(4, fileId.getTimezone());
+ assertEquals(3, fileId.getVersion());
+ assertEquals(1, fileId.getType());
+ assertEquals(8, fileId.getSubtype());
+ assertEquals(0, fileId.getDetailType());
+ }
+}