/* Copyright (C) 2019-2024 Andreas Shimokawa, Arjan Schrijver, Daniel Dakhno, Taavi Eomäe 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.qhybrid.requests.fossil.configuration; import androidx.annotation.NonNull; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.HashMap; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.file.FileHandle; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class ConfigurationPutRequest extends FilePutRequest { private static final HashMap> itemsById = new HashMap<>(); static { itemsById.put((short) 0x02, CurrentStepCountConfigItem.class); itemsById.put((short) 0x03, DailyStepGoalConfigItem.class); itemsById.put((short) 0x09, InactivityWarningItem.class); itemsById.put((short) 0x0A, VibrationStrengthConfigItem.class); itemsById.put((short) 0x0C, TimeConfigItem.class); itemsById.put((short) 0x0D, BatteryConfigItem.class); itemsById.put((short) 0x0E, HeartRateMeasurementModeItem.class); itemsById.put((short) 0x10, UnitsConfigItem.class); itemsById.put((short) 0x14, FitnessConfigItem.class); } public static ConfigItem[] parsePayload(byte[] data) { ByteBuffer buffer = ByteBuffer.wrap(data); buffer.order(ByteOrder.LITTLE_ENDIAN); ArrayList configItems = new ArrayList<>(); while (buffer.hasRemaining()) { short id = buffer.getShort(); byte length = buffer.get(); byte[] payload = new byte[length]; for (int i = 0; i < length; i++) { payload[i] = buffer.get(); } Class configClass = itemsById.get(id); if (configClass == null) { continue; } ConfigItem item; try { item = configClass.newInstance(); } catch (IllegalAccessException | InstantiationException e) { GB.log("error", GB.ERROR, e); continue; } item.parseData(payload); configItems.add(item); } return configItems.toArray(new ConfigItem[0]); } public ConfigurationPutRequest(ConfigItem item, FossilWatchAdapter adapter) { super(FileHandle.CONFIGURATION, createFileContent(new ConfigItem[]{item}), adapter); } public ConfigurationPutRequest(ConfigItem[] items, FossilWatchAdapter adapter) { super(FileHandle.CONFIGURATION, createFileContent(items), adapter); } private static byte[] createFileContent(ConfigItem[] items) { int overallSize = 0; for (ConfigItem item : items) { overallSize += item.getItemSize() + 3; } ByteBuffer buffer = ByteBuffer.allocate(overallSize); buffer.order(ByteOrder.LITTLE_ENDIAN); for (ConfigItem item : items) { buffer.putShort(item.getId()); buffer.put((byte) item.getItemSize()); buffer.put(item.getContent()); } return buffer.array(); } public static abstract class ConfigItem { public abstract int getItemSize(); public abstract short getId(); public abstract byte[] getContent(); public abstract void parseData(byte[] data); } static public class GenericConfigItem extends ConfigItem { private T value; private final short configId; public GenericConfigItem(short configId, T value) { this.value = value; this.configId = configId; } public T getValue() { return value; } @Override public int getItemSize() { switch (value.getClass().getName()) { case "java.lang.Byte": return 1; case "java.lang.Short": return 2; case "java.lang.Integer": return 4; case "java.lang.Long": return 8; } throw new UnsupportedOperationException("config type " + value.getClass().getName() + " not supported"); } @Override public short getId() { return this.configId; } @Override public byte[] getContent() { ByteBuffer buffer = ByteBuffer.allocate(getItemSize()); buffer.order(ByteOrder.LITTLE_ENDIAN); switch (value.getClass().getName()) { case "java.lang.Byte": { buffer.put((Byte) this.value); break; } case "java.lang.Integer": { buffer.putInt((Integer) this.value); break; } case "java.lang.Long": { buffer.putLong((Long) this.value); break; } case "java.lang.Short": { buffer.putShort((Short) this.value); break; } } return buffer.array(); } @Override public void parseData(byte[] data) { ByteBuffer buffer = ByteBuffer.wrap(data); buffer.order(ByteOrder.LITTLE_ENDIAN); switch (data.length) { case 1: { this.value = (T) (Byte) buffer.get(); break; } case 2: { this.value = (T) (Short) buffer.getShort(); break; } case 4: { this.value = (T) (Integer) buffer.getInt(); break; } case 8: { this.value = (T) (Long) buffer.getLong(); break; } } } } static public class BatteryConfigItem extends ConfigItem { private int batteryPercentage, batteryVoltage; public int getBatteryPercentage() { return batteryPercentage; } public int getBatteryVoltage() { return batteryVoltage; } @Override public int getItemSize() { return 3; } @Override public short getId() { return 0x0D; } @Override public byte[] getContent() { return new byte[0]; } @Override public void parseData(byte[] data) { ByteBuffer buffer = ByteBuffer.wrap(data); buffer.order(ByteOrder.LITTLE_ENDIAN); this.batteryVoltage = buffer.getShort(); this.batteryPercentage = buffer.get(); } } static public class HeartRateMeasurementModeItem extends GenericConfigItem { public HeartRateMeasurementModeItem() { this((byte) -1); } public HeartRateMeasurementModeItem(byte value) { super((short) 14, value); } } static public class DailyStepGoalConfigItem extends GenericConfigItem { public DailyStepGoalConfigItem() { this(-1); } public DailyStepGoalConfigItem(int value) { super((short) 3, value); } } static public class TimezoneOffsetConfigItem extends GenericConfigItem { public TimezoneOffsetConfigItem(Short value) { super((short) 17, value); } } static public class VibrationStrengthConfigItem extends GenericConfigItem { public VibrationStrengthConfigItem() { this((byte) -1); } public VibrationStrengthConfigItem(Byte value) { super((short) 10, value); } } static public class CurrentStepCountConfigItem extends GenericConfigItem { public CurrentStepCountConfigItem() { this(-1); } public CurrentStepCountConfigItem(Integer value) { super((short) 2, value); } } static public class UnitsConfigItem extends GenericConfigItem { public UnitsConfigItem() { this(-1); } public UnitsConfigItem(Integer value) { super((short) 16, value); } } static public class TimeConfigItem extends ConfigItem { private int epochSeconds; private short millis, offsetMinutes; public TimeConfigItem() { this(-1, (short) -1, (short) -1); } public TimeConfigItem(int epochSeconds, short millis, short offsetMinutes) { this.epochSeconds = epochSeconds; this.millis = millis; this.offsetMinutes = offsetMinutes; } @Override public int getItemSize() { return 8; } @Override public short getId() { return (short) 12; } @Override public byte[] getContent() { ByteBuffer buffer = ByteBuffer.allocate(getItemSize()); buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.putInt(this.epochSeconds); buffer.putShort(millis); buffer.putShort(offsetMinutes); return buffer.array(); } @Override public void parseData(byte[] data) { if (data.length != 8) throw new RuntimeException("wrong data"); ByteBuffer buffer = ByteBuffer.wrap(data); buffer.order(ByteOrder.LITTLE_ENDIAN); this.epochSeconds = buffer.getInt(); this.millis = buffer.getShort(); this.offsetMinutes = buffer.getShort(); } } static public class FitnessConfigItem extends ConfigItem { boolean recognizeRunning = false; boolean askRunning = false; int minutesRunning = 3; boolean recognizeBiking = false; boolean askBiking = false; int minutesBiking = 5; boolean recognizeWalking = false; boolean askWalking = false; int minutesWalking = 10; boolean recognizeRowing = false; boolean askRowing = false; int minutesRowing = 3; public FitnessConfigItem(boolean recognizeRunning, boolean askRunning, int minutesRunning, boolean recognizeBiking, boolean askBiking, int minutesBiking, boolean recognizeWalking, boolean askWalking, int minutesWalking, boolean recognizeRowing, boolean askRowing, int minutesRowing) { this.recognizeRunning = recognizeRunning; this.askRunning = askRunning; this.minutesRunning = minutesRunning; this.recognizeBiking = recognizeBiking; this.askBiking = askBiking; this.minutesBiking = minutesBiking; this.recognizeWalking = recognizeWalking; this.askWalking = askWalking; this.minutesWalking = minutesWalking; this.recognizeRowing = recognizeRowing; this.askRowing = askRowing; this.minutesRowing = minutesRowing; } public FitnessConfigItem() { } @Override public int getItemSize() { return 30; } @Override public short getId() { return 0x14; } @Override public byte[] getContent() { byte[] data = new byte[]{ // 2nd byte of each workout type is activation mode // 3rd byte of each is amount of minutes before asking/activating // Running (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x05, // Biking (byte) 0x02, (byte) 0x00, (byte) 0x05, (byte) 0x01, (byte) 0x01, (byte) 0x01, // Walking (byte) 0x08, (byte) 0x00, (byte) 0x0A, (byte) 0x01, (byte) 0x01, (byte) 0x05, // Rowing (byte) 0x09, (byte) 0x00, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x01 }; if (recognizeRunning) { data[1] |= 0x01; if (askRunning) { data[1] |= 0x02; } data[2] = (byte) (minutesRunning & 0xFF); } if (recognizeBiking) { data[7] |= 0x01; if (askBiking) { data[7] |= 0x02; } data[8] = (byte) (minutesBiking & 0xFF); } if (recognizeWalking) { data[13] |= 0x01; if (askWalking) { data[13] |= 0x02; } data[14] = (byte) (minutesWalking & 0xFF); } if (recognizeRowing) { data[19] |= 0x01; if (askRowing) { data[19] |= 0x02; } data[20] = (byte) (minutesRowing & 0xFF); } return data; } @Override public void parseData(byte[] data) { recognizeRunning = (data[1] & 0x01) == 0x01; askRunning = (data[1] & 0x02) == 0x02; minutesRunning = data[2] & 0xFF; recognizeBiking = (data[7] & 0x01) == 0x01; askBiking = (data[7] & 0x02) == 0x02; minutesBiking = data[8] & 0xFF; recognizeWalking = (data[13] & 0x01) == 0x01; askWalking = (data[13] & 0x02) == 0x02; minutesWalking = data[14] & 0xFF; recognizeRowing = (data[19] & 0x01) == 0x01; askRowing = (data[19] & 0x02) == 0x02; minutesRowing = data[20] & 0xFF; } @NonNull @Override public String toString() { return "recognizeRunning: " + recognizeRunning + " askRunning: " + askRunning + " minutesRunning: " + minutesRunning + "\n" + "recognizeBiking: " + recognizeBiking + " askBiking: " + askBiking + " minutesBiking: " + minutesBiking + "\n" + "recognizeWalking: " + recognizeWalking + " askWalking: " + askWalking + " minutesWalking: " + minutesWalking + "\n" + "recognizeRowing: " + recognizeRowing + " askRowing: " + askRowing + " minutesRowing: " + minutesRowing; } } static public class InactivityWarningItem extends ConfigItem { private int fromTimeHour, fromTimeMinute, untilTimeHour, untilTimeMinute, inactiveMinutes; boolean enabled; public InactivityWarningItem() { this(0, 0, 0, 0, 0, false); } public InactivityWarningItem(int fromTimeHour, int fromTimeMinute, int untilTimeHour, int untilTimeMinute, int inactiveMinutes, boolean enabled) { this.fromTimeHour = fromTimeHour; this.fromTimeMinute = fromTimeMinute; this.untilTimeHour = untilTimeHour; this.untilTimeMinute = untilTimeMinute; this.inactiveMinutes = inactiveMinutes; this.enabled = enabled; } @Override public int getItemSize() { return 6; } @Override public short getId() { return (short) 9; } @Override public byte[] getContent() { ByteBuffer buffer = ByteBuffer.allocate(getItemSize()); buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.put((byte) this.fromTimeHour); buffer.put((byte) this.fromTimeMinute); buffer.put((byte) this.untilTimeHour); buffer.put((byte) this.untilTimeMinute); buffer.put((byte) this.inactiveMinutes); buffer.put((byte) (this.enabled ? 1 : 0)); return buffer.array(); } @Override public void parseData(byte[] data) { if (data.length != getItemSize()) throw new RuntimeException("wrong data"); ByteBuffer buffer = ByteBuffer.wrap(data); buffer.order(ByteOrder.LITTLE_ENDIAN); this.fromTimeHour = buffer.get(); this.fromTimeMinute = buffer.get(); this.untilTimeHour = buffer.get(); this.untilTimeMinute = buffer.get(); this.inactiveMinutes = buffer.get(); this.enabled = buffer.get() == 0x01; } } }