From 544909a9708363b1cd311e31fc0eebc8d39eb2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Mon, 16 Oct 2023 21:36:23 +0100 Subject: [PATCH] Mi Band 8: Activity fetching basics --- .../xiaomi/XiaomiEncryptedSupport.java | 1 + .../activity/XiaomiActivityFileFetcher.java | 145 ++++++++++++++++++ .../xiaomi/activity/XiaomiActivityFileId.java | 125 +++++++++++++++ .../xiaomi/activity/XiaomiActivityParser.java | 46 +++++- .../services/AbstractXiaomiService.java | 2 +- .../xiaomi/services/XiaomiHealthService.java | 125 ++++++++++----- app/src/main/proto/xiaomi.proto | 11 +- .../activity/XiaomiActivityFileIdTest.java | 55 +++++++ 8 files changed, 464 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileFetcher.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileIdTest.java 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()); + } +}