1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-02 03:16:07 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java
2024-01-18 21:06:40 +00:00

271 lines
8.0 KiB
Java

/* Copyright (C) 2023-2024 Andreas Shimokawa, 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity;
import androidx.annotation.NonNull;
import org.apache.commons.lang3.builder.CompareToBuilder;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public class XiaomiActivityFileId implements Comparable<XiaomiActivityFileId> {
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 Type getType() {
return Type.fromCode(type);
}
public int getTypeCode() {
return type;
}
public Subtype getSubtype() {
return Subtype.fromCode(getType(), subtype);
}
public int getSubtypeCode() {
return subtype;
}
public DetailType getDetailType() {
return DetailType.fromCode(detailType);
}
public int getDetailTypeCode() {
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, type, subtype, detailType, version);
}
@NonNull
@Override
public String toString() {
final Type typeName = Type.fromCode(type);
final Subtype subtypeName = Subtype.fromCode(typeName, subtype);
final DetailType detailTypeName = DetailType.fromCode(detailType);
return getClass().getSimpleName() + "{" +
"timestamp=" + DateTimeUtils.formatIso8601(timestamp) +
", timezone=" + timezone +
", type=" + (typeName + String.format("(0x%02X)", type)) +
", subtype=" + (subtypeName + String.format("(0x%02X)", subtype)) +
", detailType=" + (detailTypeName + String.format("(0x%02X)", detailType)) +
", version=" + version +
"}";
}
@Override
public int compareTo(final XiaomiActivityFileId o) {
return new CompareToBuilder()
.append(timestamp, o.timestamp)
.append(timezone, o.timezone)
.append(type, o.type)
.append(subtype, o.subtype)
.append(getDetailType().getFetchOrder(), o.getDetailType().getFetchOrder())
.append(version, o.version)
.build();
}
public String getFilename() {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.US);
return String.format(
Locale.ROOT,
"xiaomi_%s_%02X_%02X_%02X_v%d.bin",
sdf.format(getTimestamp()),
getTypeCode(),
getSubtypeCode(),
getDetailTypeCode(),
getVersion()
);
}
public enum Type {
UNKNOWN(-1),
ACTIVITY(0),
SPORTS(1),
;
private final int code;
Type(final int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static Type fromCode(final int code) {
for (final Type type : values()) {
if (type.getCode() == code) {
return type;
}
}
return UNKNOWN;
}
}
public enum Subtype {
UNKNOWN(Type.UNKNOWN, -1),
ACTIVITY_DAILY(Type.ACTIVITY, 0x00),
ACTIVITY_SLEEP_STAGES(Type.ACTIVITY, 0x03),
ACTIVITY_MANUAL_SAMPLES(Type.ACTIVITY, 0x06),
ACTIVITY_SLEEP(Type.ACTIVITY, 0x08),
SPORTS_OUTDOOR_RUNNING(Type.SPORTS, 0x01),
SPORTS_OUTDOOR_WALKING_V1(Type.SPORTS, 0x02),
SPORTS_INDOOR_CYCLING(Type.SPORTS, 0x07),
SPORTS_FREESTYLE(Type.SPORTS, 0x08),
SPORTS_ELLIPTICAL(Type.SPORTS, 0x0B),
SPORTS_OUTDOOR_WALKING_V2(Type.SPORTS, 0x16),
SPORTS_OUTDOOR_CYCLING(Type.SPORTS, 0x17),
;
private final Type type;
private final int code;
Subtype(final Type type, final int code) {
this.type = type;
this.code = code;
}
public int getCode() {
return code;
}
public static Subtype fromCode(final Type type, final int code) {
for (final Subtype subtype : values()) {
if (subtype.type == type && subtype.getCode() == code) {
return subtype;
}
}
return UNKNOWN;
}
}
public enum DetailType {
UNKNOWN(-1),
DETAILS(0),
SUMMARY(0x01),
GPS_TRACK(0x02),
;
private final int code;
DetailType(final int code) {
this.code = code;
}
public int getCode() {
return code;
}
public int getFetchOrder() {
// Fetch summary first, so we have the summary data for workouts
// before parsing the gps track
switch (this) {
case SUMMARY:
return 0;
case DETAILS:
return 1;
case GPS_TRACK:
return 2;
}
return 3;
}
public static DetailType fromCode(final int code) {
for (final DetailType detailType : values()) {
if (detailType.getCode() == code) {
return detailType;
}
}
return UNKNOWN;
}
}
}