/* Copyright (C) 2023-2024 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.impl; import android.widget.Toast; 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.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiActivitySample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileId; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityParser; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class DailyDetailsParser extends XiaomiActivityParser { private static final Logger LOG = LoggerFactory.getLogger(DailyDetailsParser.class); @Override public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) { final int version = fileId.getVersion(); final int headerSize; switch (version) { case 1: case 2: headerSize = 4; break; case 3: headerSize = 5; break; case 4: headerSize = 6; break; default: LOG.warn("Unable to parse daily details version {}", fileId.getVersion()); return false; } final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); buf.limit(buf.limit() - 4); // discard crc at the end buf.get(new byte[7]); // skip fileId bytes final byte fileIdPadding = buf.get(); if (fileIdPadding != 0) { LOG.warn("Expected 0 padding after fileId, got {} - parsing might fail", fileIdPadding); } final byte[] header = new byte[headerSize]; buf.get(header); LOG.debug("Daily Details Header: {}", GB.hexdump(header)); final XiaomiComplexActivityParser complexParser = new XiaomiComplexActivityParser(header, buf); final Calendar timestamp = Calendar.getInstance(); timestamp.setTime(fileId.getTimestamp()); final List samples = new ArrayList<>(); while (buf.position() < buf.limit()) { complexParser.reset(); final XiaomiActivitySample sample = new XiaomiActivitySample(); sample.setTimestamp((int) (timestamp.getTimeInMillis() / 1000)); int includeExtraEntry = 0; if (complexParser.nextGroup(16)) { // TODO what's the first bit? if (complexParser.hasSecond()) { includeExtraEntry = complexParser.get(1, 1); } if (complexParser.hasThird()) { sample.setSteps(complexParser.get(2, 14)); } } if (complexParser.nextGroup(8)) { // TODO activity type? if (complexParser.hasSecond()) { final int calories = complexParser.get(2, 6); } } if (complexParser.nextGroup(8)) { // TODO } if (complexParser.nextGroup(16)) { // TODO distance } if (complexParser.nextGroup(8)) { if (complexParser.hasFirst()) { // hr, 8 bits sample.setHeartRate(complexParser.get(0, 8)); } } if (complexParser.nextGroup(8)) { if (complexParser.hasFirst()) { // energy, 8 bits } } if (complexParser.nextGroup(16)) { // TODO } if (version >= 3) { if (complexParser.nextGroup(8)) { if (complexParser.hasFirst()) { // spo2, 8 bits sample.setSpo2(complexParser.get(0, 8)); } } if (complexParser.nextGroup(8)) { if (complexParser.hasFirst()) { // stress, 8 bits final int stress = complexParser.get(0, 8); if (stress != 255) { sample.setStress(stress); } } } } if (includeExtraEntry == 1) { if (complexParser.nextGroup(8)) { // TODO } } if (version >= 4) { // TODO: light value (short) complexParser.nextGroup(16); // TODO: body momentum (short) complexParser.nextGroup(16); } samples.add(sample); timestamp.add(Calendar.MINUTE, 1); } // save all the samples that we got try (DBHandler handler = GBApplication.acquireDB()) { final DaoSession session = handler.getDaoSession(); final GBDevice gbDevice = support.getDevice(); final DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator(); final SampleProvider sampleProvider = (SampleProvider) coordinator.getSampleProvider(gbDevice, session); final Device device = DBHelper.getDevice(gbDevice, session); final User user = DBHelper.getUser(session); for (final XiaomiActivitySample sample : samples) { sample.setDevice(device); sample.setUser(user); sample.setProvider(sampleProvider); } sampleProvider.addGBActivitySamples(samples.toArray(new XiaomiActivitySample[0])); return true; } catch (final Exception e) { GB.toast(support.getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR); LOG.error("Error saving activity samples", e); return false; } } }