mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-06-02 03:16:07 +02:00
271 lines
8.0 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|