/* Copyright (C) 2024 Damien Gaignon, Martin.JM
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.huawei;
import android.content.Context;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import de.greenrobot.dao.query.CloseableListIterator;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryProgressEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryTableRowEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryValue;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSportHRZones;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSwimSegmentsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSwimSegmentsSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
/**
* This class parses the Huawei workouts into the table GB uses to show the workouts
* It also re-parses the unknown data from the workout tables
* It is a separate class so it can easily be used to re-parse the data without database migrations
*/
public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWorkoutGbParser.class);
// TODO: Might be nicer to propagate the exceptions, so they can be handled upstream
private final GBDevice gbDevice;
private final Context context;
public HuaweiWorkoutGbParser(final GBDevice gbDevice, final Context context) {
this.gbDevice = gbDevice;
this.context = context;
}
@Override
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) {
if (!forDetails) {
// Our parsing is too slow, especially without a RecyclerView
return summary;
}
// Find the existing HuaweiWorkoutSummarySample
try (DBHandler db = GBApplication.acquireDB()) {
final DaoSession session = db.getDaoSession();
final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session);
QueryBuilder qb = session.getHuaweiWorkoutSummarySampleDao().queryBuilder();
qb.where(HuaweiWorkoutSummarySampleDao.Properties.StartTimestamp.eq(summary.getStartTime().getTime() / 1000));
qb.where(HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(device.getId()));
qb.where(HuaweiWorkoutSummarySampleDao.Properties.UserId.eq(user.getId()));
final List huaweiSummaries = qb.build().list();
if (huaweiSummaries.isEmpty()) {
LOG.warn("Failed to find huawei summary for {}", summary.getStartTime());
return summary;
}
updateBaseSummary(session, huaweiSummaries.get(0), summary);
} catch (Exception e) {
LOG.error("Failed to update summary");
}
return summary;
}
public enum HuaweiActivityType {
// Based on nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport
RUNNING(1, ActivityKind.RUNNING),
WALKING(2, ActivityKind.WALKING),
CYCLING(3, ActivityKind.CYCLING),
MOUNTAIN_HIKE(4, ActivityKind.MOUNTAIN_HIKE),
INDOOR_RUN(5, ActivityKind.INDOOR_RUNNING),
POOL_SWIM(6, ActivityKind.POOL_SWIM),
INDOOR_CYCLE(7, ActivityKind.INDOOR_CYCLING),
OPEN_WATER_SWIM(8, ActivityKind.SWIMMING_OPENWATER),
INDOOR_WALK(13, ActivityKind.INDOOR_WALKING),
HIKING(14, ActivityKind.HIKING),
JUMP_ROPING(21, ActivityKind.JUMP_ROPING),
PING_PONG(128, ActivityKind.PINGPONG),
BADMINTON(129, ActivityKind.BADMINTON),
TENNIS(130, ActivityKind.TENNIS),
SOCCER(131, ActivityKind.SOCCER),
BASKETBALL(132, ActivityKind.BASKETBALL),
VOLLEYBALL(133, ActivityKind.VOLLEYBALL),
ELLIPTICAL_TRAINER(134, ActivityKind.ELLIPTICAL_TRAINER),
ROWING_MACHINE(135, ActivityKind.ROWING_MACHINE),
STEPPER(136, ActivityKind.STEPPER),
YOGA(137, ActivityKind.YOGA),
PILATES(138, ActivityKind.PILATES),
AEROBICS(139, ActivityKind.AEROBICS),
STRENGTH_TRAINING(140, ActivityKind.STRENGTH_TRAINING),
SPINNING(141, ActivityKind.SPINNING),
AIR_WALKER(142, ActivityKind.AIR_WALKER),
HIIT(143, ActivityKind.HIIT),
CROSSFIT(145, ActivityKind.CROSSFIT),
FUNCTIONAL_TRAINING(146, ActivityKind.FUNCTIONAL_TRAINING),
PHYSICAL_TRAINING(147, ActivityKind.PHYSICAL_TRAINING),
TAEKWONDO(148, ActivityKind.TAEKWONDO),
BOXING(149, ActivityKind.BOXING),
FREE_SPARRING(150, ActivityKind.FREE_SPARRING),
KARATE(151, ActivityKind.KARATE),
FENCING(152, ActivityKind.FENCING),
BELLY_DANCE(153, ActivityKind.BELLY_DANCE),
JAZZ_DANCE(154, ActivityKind.JAZZ_DANCE),
LATIN_DANCE(155, ActivityKind.LATIN_DANCE),
BALLET(156, ActivityKind.BALLET),
CORE_TRAINING(157, ActivityKind.CORE_TRAINING),
BODY_COMBAT(158, ActivityKind.BODY_COMBAT),
KENDO(159, ActivityKind.KENDO),
SINGLE_BAR(160, ActivityKind.HORIZONTAL_BAR),
PARALLEL_BARS(161, ActivityKind.PARALLEL_BARS),
STREET_DANCE(162, ActivityKind.STREET_DANCE),
ROLLER_SKATING(163, ActivityKind.ROLLER_SKATING),
MARTIAL_ARTS(164, ActivityKind.MARTIAL_ARTS),
PLAZA_DANCING(165, ActivityKind.PLAZA_DANCING),
TAI_CHI(166, ActivityKind.TAI_CHI),
DANCE(167, ActivityKind.DANCE),
HULA_HOOP(168, ActivityKind.HULA_HOOP),
FRISBEE(169, ActivityKind.FRISBEE),
DARTS(170, ActivityKind.DARTS),
ARCHERY(171, ActivityKind.ARCHERY),
HORSE_RIDING(172, ActivityKind.HORSE_RIDING),
LASER_TAG(173, ActivityKind.LASER_TAG),
KITE_FLYING(174, ActivityKind.KITE_FLYING),
TUG_OF_WAR(175, ActivityKind.TUG_OF_WAR),
SWINGING(176, ActivityKind.SWING),
STAIR_CLIMBING(177, ActivityKind.STAIRS),
OBSTACLE_RACE(178, ActivityKind.OBSTACLE_RACE),
BILLIARD_POOL(179, ActivityKind.BILLIARD_POOL),
BOWLING(180, ActivityKind.BOWLING),
SHUTTLECOCK(181, ActivityKind.SHUTTLECOCK),
HANDBALL(182, ActivityKind.HANDBALL),
BASEBALL(183, ActivityKind.BASEBALL),
SOFTBALL(184, ActivityKind.SOFTBALL),
CRICKET(185, ActivityKind.CRICKET),
RUGBY(186, ActivityKind.RUGBY),
BEACH_SOCCER(187, ActivityKind.BEACH_SOCCER),
BEACH_VOLLEYBALL(188, ActivityKind.BEACH_VOLLEYBALL),
GATEBALL(189, ActivityKind.GATEBALL),
HOCKEY(190, ActivityKind.HOCKEY),
SQUASH(191, ActivityKind.SQUASH),
SEPAK_TAKRAW(192, ActivityKind.SEPAK_TAKRAW),
DODGEBALL(193, ActivityKind.DODGEBALL),
SAILING(194, ActivityKind.SAILING),
SURFING(195, ActivityKind.SURFING),
FISHING(196, ActivityKind.FISHING),
RAFTING(197, ActivityKind.RAFTING),
DRAGON_BOATING(198, ActivityKind.DRAGON_BOAT),
CANOEING(199, ActivityKind.CANOEING),
ROWING(200, ActivityKind.ROWING),
WATER_SCOOTER(201, ActivityKind.WATER_SCOOTER),
STAND_UP_PADDLEBOARDING(202, ActivityKind.STAND_UP_PADDLEBOARDING),
ICE_SKATING(203, ActivityKind.ICE_SKATING),
ICE_HOCKEY(204, ActivityKind.ICE_HOCKEY),
CURLING(205, ActivityKind.CURLING),
BOBSLEIGH(206, ActivityKind.BOBSLEIGH),
SLEDDING(207, ActivityKind.SLEDDING),
BIATHLON(208, ActivityKind.BIATHLON),
SKATEBOARDING(209, ActivityKind.SKATEBOARDING),
ROCK_CLIMBING(210, ActivityKind.ROCK_CLIMBING),
BUNGEE_JUMPING(211, ActivityKind.BUNGEE_JUMPING),
PARKOUR(212, ActivityKind.PARKOUR),
BMX(213, ActivityKind.BMX),
ORIENTEERING(214, ActivityKind.ORIENTEERING),
PARACHUTING(215, ActivityKind.PARACHUTING),
MOTOR_AUTO_RACING(216, ActivityKind.AUTO_RACING),
ESPORTS(223, ActivityKind.ESPORTS),
PADEL(224, ActivityKind.PADEL),
OTHER(255, ActivityKind.EXERCISE),
;
private final byte type;
private final ActivityKind activityKind;
HuaweiActivityType(final int type, final ActivityKind activityKind) {
this.type = (byte) type;
this.activityKind = activityKind;
}
public ActivityKind getActivityKind() {
return activityKind;
}
public byte getType() {
return type;
}
public static Optional fromByte(final byte type) {
for (final HuaweiActivityType value : HuaweiActivityType.values()) {
if (value.getType() == type)
return Optional.of(value);
}
return Optional.empty();
}
}
public void parseAllWorkouts() {
parseUnknownWorkoutData();
try (DBHandler db = GBApplication.acquireDB()) {
final DaoSession session = db.getDaoSession();
QueryBuilder qb = session.getHuaweiWorkoutSummarySampleDao().queryBuilder();
for (HuaweiWorkoutSummarySample summary : qb.listLazy()) {
parseWorkout(session, summary.getWorkoutId(), summary.getDeviceId());
}
} catch (Exception e) {
GB.toast("Exception parsing workouts", Toast.LENGTH_SHORT, GB.ERROR, e);
LOG.error("Exception parsing workouts", e);
}
}
/**
* Parses the unknown data from the workout data table
*/
private static void parseUnknownWorkoutData() {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
QueryBuilder qb = dbHandler.getDaoSession().getHuaweiWorkoutDataSampleDao().queryBuilder().where(
HuaweiWorkoutDataSampleDao.Properties.DataErrorHex.notEq("")
);
for (HuaweiWorkoutDataSample sample : qb.build().listLazy()) {
byte[] data = GB.hexStringToByteArray(new String(sample.getDataErrorHex()));
Workout.WorkoutData.Response response = new Workout.WorkoutData.Response(data);
for (Workout.WorkoutData.Response.Data responseData : response.dataList) {
byte[] dataErrorHex;
if (responseData.unknownData == null)
dataErrorHex = null;
else
dataErrorHex = StringUtils.bytesToHex(responseData.unknownData).getBytes(StandardCharsets.UTF_8);
HuaweiWorkoutDataSample dataSample = new HuaweiWorkoutDataSample(
sample.getWorkoutId(),
responseData.timestamp,
responseData.heartRate,
responseData.speed,
responseData.stepRate,
responseData.cadence,
responseData.stepLength,
responseData.groundContactTime,
responseData.impact,
responseData.swingAngle,
responseData.foreFootLanding,
responseData.midFootLanding,
responseData.backFootLanding,
responseData.eversionAngle,
responseData.swolf,
responseData.strokeRate,
dataErrorHex,
responseData.calories,
responseData.cyclingPower,
responseData.frequency,
responseData.altitude
);
dbHandler.getDaoSession().getHuaweiWorkoutDataSampleDao().insertOrReplace(dataSample);
}
}
} catch (Exception e) {
GB.toast("Exception parsing unknown workout data", Toast.LENGTH_SHORT, GB.ERROR, e);
LOG.error("Exception parsing unknown workout data", e);
}
}
public static ActivityKind huaweiTypeToGbType(byte huaweiType) {
final Optional type = HuaweiActivityType.fromByte(huaweiType);
if (type.isPresent())
return type.get().getActivityKind();
else
return ActivityKind.UNKNOWN;
}
private String getSwimStyle(byte swimType) {
switch (swimType) {
case 1:
return "breaststroke";
case 3:
return "butterfly";
case 4:
return "backstroke";
case 5:
return "medley";
}
return "freestyle";
}
public void parseWorkout(Long workoutId) {
LOG.debug("Parsing workout ID {}", workoutId);
if (workoutId == null)
return;
try (DBHandler db = GBApplication.acquireDB()) {
final DaoSession session = db.getDaoSession();
final Device device = DBHelper.getDevice(gbDevice, session);
parseWorkout(session, workoutId, device.getId());
} catch (Exception e) {
GB.toast("Exception parsing workout data", Toast.LENGTH_SHORT, GB.ERROR, e);
LOG.error("Exception parsing workout data", e);
}
}
public void parseWorkout(final DaoSession session, final Long workoutId, final long deviceId) {
if (workoutId == null)
return;
QueryBuilder qbSummary = session.getHuaweiWorkoutSummarySampleDao().queryBuilder().where(
HuaweiWorkoutSummarySampleDao.Properties.WorkoutId.eq(workoutId)
);
List summarySamples = qbSummary.build().list();
if (summarySamples.size() != 1)
return;
HuaweiWorkoutSummarySample summary = summarySamples.get(0);
final BaseActivitySummary baseSummary = ActivitySummaryParser.findOrCreateBaseActivitySummary(
session,
deviceId,
summary.getStartTimestamp()
);
updateBaseSummary(session, summary, baseSummary);
session.getBaseActivitySummaryDao().insertOrReplace(baseSummary);
}
public void updateBaseSummary(final DaoSession session,
final HuaweiWorkoutSummarySample summary,
final BaseActivitySummary baseSummary) {
try {
QueryBuilder qbData = session.getHuaweiWorkoutDataSampleDao().queryBuilder().where(
HuaweiWorkoutDataSampleDao.Properties.WorkoutId.eq(summary.getWorkoutId())
);
List dataSamples = qbData.build().list();
QueryBuilder qbPace = session.getHuaweiWorkoutPaceSampleDao().queryBuilder().orderAsc(HuaweiWorkoutPaceSampleDao.Properties.PaceIndex).where(
HuaweiWorkoutPaceSampleDao.Properties.WorkoutId.eq(summary.getWorkoutId())
);
QueryBuilder qbSegments = session.getHuaweiWorkoutSwimSegmentsSampleDao().queryBuilder().orderAsc(HuaweiWorkoutSwimSegmentsSampleDao.Properties.SegmentIndex).where(
HuaweiWorkoutSwimSegmentsSampleDao.Properties.WorkoutId.eq(summary.getWorkoutId())
);
ActivityKind type = huaweiTypeToGbType(summary.getType());
ActivitySummaryData summaryData = new ActivitySummaryData();
summaryData.add(ActivitySummaryEntries.CALORIES_BURNT, summary.getCalories(), ActivitySummaryEntries.UNIT_KCAL);
summaryData.add(ActivitySummaryEntries.DISTANCE_METERS, summary.getDistance(), ActivitySummaryEntries.UNIT_METERS);
summaryData.add(ActivitySummaryEntries.STEPS, summary.getStepCount(), ActivitySummaryEntries.UNIT_STEPS);
summaryData.add(ActivitySummaryEntries.ACTIVE_SECONDS, summary.getDuration(), ActivitySummaryEntries.UNIT_SECONDS);
//summaryData.add(ActivitySummaryEntries.STATUS, summary.getStatus() & 0xFF, ActivitySummaryEntries.UNIT_NONE);
summaryData.add(ActivitySummaryEntries.TYPE, summary.getType() & 0xFF, ActivitySummaryEntries.UNIT_NONE);
if (summary.getStrokes() != -1) {
summaryData.add(ActivitySummaryEntries.STROKES, summary.getStrokes(), ActivitySummaryEntries.UNIT_STROKES);
}
if (summary.getPoolLength() != -1) {
summaryData.add(ActivitySummaryEntries.LANE_LENGTH, summary.getPoolLength(), ActivitySummaryEntries.UNIT_CM);
}
if (summary.getLaps() != -1) {
summaryData.add(ActivitySummaryEntries.LAPS, summary.getLaps(), ActivitySummaryEntries.UNIT_LAPS);
}
if(summary.getWorkoutLoad() > 0) {
summaryData.add(ActivitySummaryEntries.WORKOUT_LOAD, summary.getWorkoutLoad(), ActivitySummaryEntries.UNIT_NONE);
}
if(summary.getWorkoutAerobicEffect() > 0) {
summaryData.add(ActivitySummaryEntries.TRAINING_EFFECT_AEROBIC, summary.getWorkoutAerobicEffect() / 10.0, ActivitySummaryEntries.UNIT_NONE);
}
if(summary.getWorkoutAnaerobicEffect() >= 0) {
summaryData.add(ActivitySummaryEntries.TRAINING_EFFECT_ANAEROBIC, summary.getWorkoutAnaerobicEffect() / 10.0, ActivitySummaryEntries.UNIT_NONE);
}
if(summary.getRecoveryTime() > 0) {
summaryData.add(ActivitySummaryEntries.RECOVERY_TIME, summary.getRecoveryTime() / 60.0, ActivitySummaryEntries.UNIT_HOURS);
}
if(summary.getSwimType() != -1) {
summaryData.add(ActivitySummaryEntries.SWIM_STYLE, getSwimStyle(summary.getSwimType()));
}
if(summary.getMaxMET() > 0) {
int value = (int) (((float) summary.getMaxMET() * 3.5f)) / 65536;
summaryData.add(ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE, value, ActivitySummaryEntries.UNIT_ML_KG_MIN);
}
Integer summaryMinAltitude = summary.getMinAltitude();
Integer summaryMaxAltitude = summary.getMaxAltitude();
Integer elevationGain = summary.getElevationGain();
Integer elevationLoss = summary.getElevationLoss();
int minHeartRatePeak = summary.getMinHeartRatePeak() & 0xff;
int maxHeartRatePeak = summary.getMaxHeartRatePeak() & 0xff;
int avgStrokeRate = summary.getAvgStrokeRate();
int avgSwolf = summary.getAvgSwolf();
boolean unknownData = false;
if (!dataSamples.isEmpty()) {
int speed = 0;
int speedCount = 0;
boolean stepRatePresent = false;
int stepRate = 0;
int avgStepRate = 0;
int cadence = 0;
int cadenceCount = 0;
int stepLength = 0;
int stepLengthCount = 0;
int groundContactTime = 0;
int groundContactTimeCount = 0;
int impact = 0;
int impactCount = 0;
int maxImpact = 0;
int swingAngle = 0;
int swingAngleCount = 0;
boolean footLandingPresent = false;
int foreFootLanding = 0;
int midFootLanding = 0;
int backFootLanding = 0;
int eversionAngle = 0;
int eversionAngleCount = 0;
int maxEversionAngle = 0;
int swolf = 0;
int swolfCount = 0;
int maxSwolf = 0;
int minSwolf = 0;
int strokeRate = 0;
int strokeRateCount = 0;
int maxStrokeRate = 0;
int heartRate = 0;
int heartRateCount = 0;
int maxHeartRate = 0;
int minHeartRate = Integer.MAX_VALUE;
int sumCalories = 0;
int minCyclingPower = Integer.MAX_VALUE;
int maxCyclingPower = 0;
int cyclingPower = 0;
int cyclingPowerCount = 0;
int avgAltitude = 0;
int altitudeCount = 0;
int minAltitude = Integer.MAX_VALUE;
int maxAltitude = Integer.MIN_VALUE;
Integer previousAlt = null;
int sumAltitudeUp = 0;
int sumAltitudeDown = 0;
//NOTE: The method of retrieving HR zones from the Huawei watch is not discovered. It may not return zones.
// So they are calculated based on config.
HeartRateZonesConfig HRZonesCfg = null;
Integer zoneType = HuaweiWorkoutUtils.getHRZoneTypeByActivity(type);
int zoneCalculateMethod = summary.getHrZoneType();
LOG.info("Workout HR Zone Calculate Type: {}", zoneCalculateMethod);
if(zoneType != null && HeartRateZonesConfig.isCalculateMethodValidFroType(zoneType, zoneCalculateMethod)) {
ActivityUser activityUser = new ActivityUser();
HuaweiSportHRZones hrSportZones = new HuaweiSportHRZones(activityUser.getAge());
HRZonesCfg = hrSportZones.getHRZonesConfigByType(zoneType);
}
int dataDelta = 5;
if (dataSamples.size() >= 2 && dataSamples.get(1).getTimestamp() - dataSamples.get(0).getTimestamp() >= 40) {
dataDelta = 60;
}
int[] HRZones = new int[5];
int dataIdx = 0;
for (HuaweiWorkoutDataSample dataSample : dataSamples) {
if(HRZonesCfg != null) {
int zoneIdx = HRZonesCfg.getZoneByMethod(dataSample.getHeartRate() & 0xFF, zoneCalculateMethod);
if (zoneIdx != -1 && dataIdx < (dataSamples.size() - 1)) {
HRZones[zoneIdx] += dataDelta;
}
dataIdx++;
}
if (dataSample.getSpeed() != -1) {
speed += dataSample.getSpeed();
speedCount += 1;
}
if (dataSample.getStepRate() != -1) {
stepRate += dataSample.getStepRate();
stepRatePresent = true;
}
if (dataSample.getCadence() != -1) {
cadence += dataSample.getCadence();
cadenceCount += 1;
}
if (dataSample.getStepLength() != -1) {
stepLength += dataSample.getStepLength();
stepLengthCount += 1;
}
if (dataSample.getGroundContactTime() != -1) {
groundContactTime += dataSample.getGroundContactTime();
groundContactTimeCount += 1;
}
if (dataSample.getImpact() != -1) {
impact += dataSample.getImpact();
impactCount += 1;
if (dataSample.getImpact() > maxImpact)
maxImpact = dataSample.getImpact();
}
if (dataSample.getSwingAngle() != -1) {
swingAngle += dataSample.getSwingAngle();
swingAngleCount += 1;
}
if (dataSample.getForeFootLanding() != -1) {
foreFootLanding += dataSample.getForeFootLanding();
footLandingPresent = true;
}
if (dataSample.getMidFootLanding() != -1) {
midFootLanding += dataSample.getMidFootLanding();
footLandingPresent = true;
}
if (dataSample.getBackFootLanding() != -1) {
backFootLanding += dataSample.getBackFootLanding();
footLandingPresent = true;
}
if (dataSample.getEversionAngle() != -1) {
eversionAngle += dataSample.getEversionAngle();
eversionAngleCount += 1;
if (dataSample.getEversionAngle() > maxEversionAngle)
maxEversionAngle = dataSample.getEversionAngle();
}
if (dataSample.getSwolf() != -1) {
swolf += dataSample.getSwolf();
swolfCount += 1;
if (dataSample.getSwolf() > maxSwolf)
maxSwolf = dataSample.getSwolf();
if (dataSample.getSwolf() < minSwolf)
minSwolf = dataSample.getSwolf();
}
if (dataSample.getStrokeRate() != -1) {
strokeRate += dataSample.getStrokeRate();
strokeRateCount += 1;
if (dataSample.getStrokeRate() > maxStrokeRate)
maxStrokeRate = dataSample.getStrokeRate();
}
if (dataSample.getHeartRate() != -1 && dataSample.getHeartRate() != 0) {
int hr = dataSample.getHeartRate() & 0xff;
heartRate += hr;
heartRateCount += 1;
if (hr > maxHeartRate)
maxHeartRate = hr;
if (hr < minHeartRate)
minHeartRate = hr;
}
if (dataSample.getCalories() != -1)
sumCalories += dataSample.getCalories();
if (dataSample.getCyclingPower() != -1) {
int cp = dataSample.getCyclingPower();
cyclingPower += cp;
cyclingPowerCount += 1;
if (cp > maxCyclingPower)
maxCyclingPower = cp;
if (cp < minCyclingPower)
minCyclingPower = cp;
}
if (dataSample.getAltitude() != null) {
int alt = dataSample.getAltitude();
avgAltitude += alt;
altitudeCount += 1;
if (alt > maxAltitude)
maxAltitude = alt;
if (alt < minAltitude)
minAltitude = alt;
if (previousAlt != null) {
if (alt > previousAlt)
sumAltitudeUp += alt - previousAlt;
else if (alt < previousAlt)
sumAltitudeDown += previousAlt - alt;
}
previousAlt = alt;
}
if (dataSample.getDataErrorHex() != null)
unknownData = true;
}
if(HRZonesCfg != null) {
final double totalTime = Arrays.stream(HRZones).sum();
final List zoneOrder = Arrays.asList(ActivitySummaryEntries.HR_ZONE_WARM_UP, ActivitySummaryEntries.HR_ZONE_FAT_BURN, ActivitySummaryEntries.HR_ZONE_AEROBIC, ActivitySummaryEntries.HR_ZONE_ANAEROBIC, ActivitySummaryEntries.HR_ZONE_EXTREME);
final int[] zoneColors = new int[]{
context.getResources().getColor(R.color.hr_zone_warm_up_color),
context.getResources().getColor(R.color.hr_zone_easy_color),
context.getResources().getColor(R.color.hr_zone_aerobic_color),
context.getResources().getColor(R.color.hr_zone_threshold_color),
context.getResources().getColor(R.color.hr_zone_maximum_color),
};
for (int i = zoneOrder.size() - 1; i >= 0; i--) {
double timeInZone = HRZones[i];
LOG.info("Zone: {} {}", zoneOrder.get(i), timeInZone);
summaryData.add(
zoneOrder.get(i),
new ActivitySummaryProgressEntry(
timeInZone,
ActivitySummaryEntries.UNIT_SECONDS,
(int) (100 * timeInZone / totalTime),
zoneColors[i]
)
);
}
}
// Average the things that should be averaged
if (speedCount > 0)
speed = speed / speedCount;
if (cadenceCount > 0)
cadence = cadence / cadenceCount;
if (summary.getDuration() > 60)
avgStepRate = stepRate / (summary.getDuration() / 60); // steps per minute
if (stepLengthCount > 0)
stepLength = stepLength / stepLengthCount;
if (groundContactTimeCount > 0)
groundContactTime = groundContactTime / groundContactTimeCount;
if (impactCount > 0)
impact = impact / impactCount;
if (swingAngleCount > 0)
swingAngle = swingAngle / swingAngleCount;
if (eversionAngleCount > 0)
eversionAngle = eversionAngle / eversionAngleCount;
if (swolfCount > 0)
swolf = swolf / swolfCount;
if (strokeRateCount > 0)
strokeRate = strokeRate / strokeRateCount;
if (heartRateCount > 0)
heartRate = heartRate / heartRateCount;
if (cyclingPowerCount > 0)
cyclingPower = cyclingPower / cyclingPowerCount;
if (altitudeCount > 0)
avgAltitude = avgAltitude / altitudeCount;
if (speedCount > 0) {
summaryData.add(ActivitySummaryEntries.SPEED_AVG, speed / 10f, ActivitySummaryEntries.UNIT_METERS_PER_SECOND);
}
if (stepRatePresent) {
summaryData.add(ActivitySummaryEntries.STEP_RATE_AVG, avgStepRate, ActivitySummaryEntries.UNIT_SPM);
}
if (cadenceCount > 0) {
summaryData.add(ActivitySummaryEntries.CADENCE_AVG, cadence, ActivitySummaryEntries.UNIT_SPM);
}
if (stepLengthCount > 0) {
summaryData.add(ActivitySummaryEntries.STEP_LENGTH_AVG, stepLength, ActivitySummaryEntries.UNIT_CM);
}
if (groundContactTimeCount > 0) {
summaryData.add(ActivitySummaryEntries.GROUND_CONTACT_TIME_AVG, groundContactTime, ActivitySummaryEntries.UNIT_MILLISECONDS);
}
if (impactCount > 0) {
summaryData.add(ActivitySummaryEntries.IMPACT_AVG, impact, "g");
summaryData.add(ActivitySummaryEntries.IMPACT_MAX, maxImpact, "g");
}
if (swingAngleCount > 0) {
summaryData.add(ActivitySummaryEntries.SWING_ANGLE_AVG, swingAngle, ActivitySummaryEntries.UNIT_DEGREES);
}
if (footLandingPresent) {
summaryData.add(ActivitySummaryEntries.FORE_FOOT_LANDINGS, foreFootLanding, ActivitySummaryEntries.UNIT_NONE);
summaryData.add(ActivitySummaryEntries.MID_FOOT_LANDINGS, midFootLanding, ActivitySummaryEntries.UNIT_NONE);
summaryData.add(ActivitySummaryEntries.BACK_FOOT_LANDINGS, backFootLanding, ActivitySummaryEntries.UNIT_NONE);
}
if (eversionAngleCount > 0) {
summaryData.add(ActivitySummaryEntries.EVERSION_ANGLE_AVG, eversionAngle, ActivitySummaryEntries.UNIT_DEGREES);
summaryData.add(ActivitySummaryEntries.EVERSION_ANGLE_MAX, maxEversionAngle, ActivitySummaryEntries.UNIT_DEGREES);
}
if (swolfCount > 0) {
if(avgSwolf == -1) {
avgSwolf = swolf;
}
summaryData.add(ActivitySummaryEntries.SWOLF_MAX, maxSwolf, ActivitySummaryEntries.UNIT_NONE);
summaryData.add(ActivitySummaryEntries.SWOLF_MIN, minSwolf, ActivitySummaryEntries.UNIT_NONE);
}
if (strokeRateCount > 0) {
if(avgStrokeRate == -1) {
avgStrokeRate = strokeRate;
}
summaryData.add(ActivitySummaryEntries.STROKE_RATE_MAX, maxStrokeRate, ActivitySummaryEntries.UNIT_STROKES_PER_MINUTE);
}
if (heartRateCount > 0) {
summaryData.add(ActivitySummaryEntries.HR_AVG, heartRate, ActivitySummaryEntries.UNIT_BPM);
if(minHeartRatePeak == 0) {
minHeartRatePeak = minHeartRate;
}
if(maxHeartRatePeak == 0) {
maxHeartRatePeak = maxHeartRate;
}
}
if (sumCalories > 0) {
summaryData.add(ActivitySummaryEntries.CALORIES_BURNT, sumCalories, ActivitySummaryEntries.UNIT_KCAL);
}
if (cyclingPowerCount > 0) {
summaryData.add(ActivitySummaryEntries.CYCLING_POWER_AVERAGE, cyclingPower, ActivitySummaryEntries.UNIT_NONE);
summaryData.add(ActivitySummaryEntries.CYCLING_POWER_MIN, minCyclingPower, ActivitySummaryEntries.UNIT_NONE);
summaryData.add(ActivitySummaryEntries.CYCLING_POWER_MAX, maxCyclingPower, ActivitySummaryEntries.UNIT_NONE);
}
if (altitudeCount > 0) {
summaryData.add(ActivitySummaryEntries.ALTITUDE_AVG, avgAltitude / 10.0f, ActivitySummaryEntries.UNIT_METERS);
if(summaryMinAltitude == null) {
summaryMinAltitude = minAltitude;
}
if(summaryMaxAltitude == null) {
summaryMaxAltitude = maxAltitude;
}
if(elevationGain == null) {
elevationGain = sumAltitudeUp;
}
if(elevationLoss == null) {
elevationLoss = sumAltitudeDown;
}
}
}
if(avgSwolf > 0) {
summaryData.add(ActivitySummaryEntries.SWOLF_AVG, avgSwolf, ActivitySummaryEntries.UNIT_NONE);
}
if (avgStrokeRate > 0) {
summaryData.add(ActivitySummaryEntries.STROKE_RATE_AVG, avgStrokeRate, ActivitySummaryEntries.UNIT_STROKES_PER_MINUTE);
}
if (minHeartRatePeak > 0) {
summaryData.add(ActivitySummaryEntries.HR_MIN, minHeartRatePeak, ActivitySummaryEntries.UNIT_BPM);
}
if (maxHeartRatePeak > 0) {
summaryData.add(ActivitySummaryEntries.HR_MAX, maxHeartRatePeak, ActivitySummaryEntries.UNIT_BPM);
}
if(summaryMinAltitude != null) {
summaryData.add(ActivitySummaryEntries.ALTITUDE_MIN, summaryMinAltitude / 10.0f, ActivitySummaryEntries.UNIT_METERS);
}
if(summaryMaxAltitude != null) {
summaryData.add(ActivitySummaryEntries.ALTITUDE_MAX, summaryMaxAltitude / 10.0f, ActivitySummaryEntries.UNIT_METERS);
}
if(elevationGain != null) {
summaryData.add(ActivitySummaryEntries.ELEVATION_GAIN, elevationGain / 10.0f, ActivitySummaryEntries.UNIT_METERS);
}
if(elevationLoss != null) {
summaryData.add(ActivitySummaryEntries.ELEVATION_LOSS, elevationLoss / 10.0f, ActivitySummaryEntries.UNIT_METERS);
}
final LinkedHashMap pacesTable = new LinkedHashMap<>();
pacesTable.put("paces_table",
new ActivitySummaryTableRowEntry(
ActivitySummaryEntries.GROUP_PACE,
Arrays.asList(
new ActivitySummaryValue("#", ActivitySummaryEntries.UNIT_RAW_STRING),
new ActivitySummaryValue("distance"),
new ActivitySummaryValue("Pace")
),
true,
true
)
);
String measurementSystem = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, "metric");
byte unitType = (byte) (measurementSystem.equals("metric") ? 0 : 1);
try (CloseableListIterator it = qbPace.build().listIterator()) {
int paceCount = 0;
int paceSum = 0;
int paceFastest = Integer.MAX_VALUE;
int paceSlowest = 0;
int currentIndex = 1;
while (it.hasNext()) {
int index = it.nextIndex();
HuaweiWorkoutPaceSample sample = it.next();
if (sample.getType() != unitType)
continue;
int pace = sample.getPace();
paceCount++;
paceSum += pace;
if (pace < paceFastest)
paceFastest = pace;
if (pace > paceSlowest)
paceSlowest = pace;
double distance = sample.getDistance();
if (sample.getCorrection() != null) {
distance += sample.getCorrection() / 10000d;
}
final List columns = new LinkedList<>();
// TODO: add proper units for type == 1. MILES and SECONDS PER MILE
columns.add(new ActivitySummaryValue(currentIndex++, ActivitySummaryEntries.UNIT_NONE));
columns.add(new ActivitySummaryValue(distance, ActivitySummaryEntries.UNIT_KILOMETERS));
columns.add(new ActivitySummaryValue(sample.getPace(), ActivitySummaryEntries.UNIT_SECONDS_PER_KM));
pacesTable.put("paces_table_" + index,
new ActivitySummaryTableRowEntry(
ActivitySummaryEntries.GROUP_PACE,
columns,
false,
true
)
);
}
if (paceCount != 0 && paceSum != 0) {
summaryData.add(
ActivitySummaryEntries.GROUP_PACE,
GBApplication.getContext().getString(R.string.fmtPaceAverage),
paceSum / (float) paceCount,
ActivitySummaryEntries.UNIT_SECONDS_PER_KM
);
}
if (paceFastest != Integer.MAX_VALUE) {
summaryData.add(
ActivitySummaryEntries.GROUP_PACE,
GBApplication.getContext().getString(R.string.maxPace),
paceFastest,
ActivitySummaryEntries.UNIT_SECONDS_PER_KM
);
}
if (paceSlowest != 0) {
summaryData.add(
ActivitySummaryEntries.GROUP_PACE,
GBApplication.getContext().getString(R.string.minPace),
paceSlowest,
ActivitySummaryEntries.UNIT_SECONDS_PER_KM
);
}
if (pacesTable.size() > 1) {
for (final Map.Entry e : pacesTable.entrySet()) {
summaryData.add(e.getKey(), e.getValue());
}
}
}
final LinkedHashMap segmentsTable = new LinkedHashMap<>();
try (CloseableListIterator it = qbSegments.build().listIterator()) {
int currentIndex = 1;
int tableIndex = 1;
while (it.hasNext()) {
HuaweiWorkoutSwimSegmentsSample sample = it.next();
if (sample.getType() != unitType)
continue;
final List columns = new LinkedList<>();
// TODO: add proper units for type == 1. MILES
columns.add(new ActivitySummaryValue(currentIndex++, ActivitySummaryEntries.UNIT_NONE));
columns.add(new ActivitySummaryValue(getSwimStyle(sample.getSwimType()), ActivitySummaryEntries.UNIT_NONE));
columns.add(new ActivitySummaryValue(sample.getDistance(), ActivitySummaryEntries.UNIT_METERS));
columns.add(new ActivitySummaryValue(sample.getTime(), ActivitySummaryEntries.UNIT_SECONDS));
segmentsTable.put("segments_table_" + tableIndex++,
new ActivitySummaryTableRowEntry(
ActivitySummaryEntries.GROUP_PACE,
columns,
true,
true
)
);
final List columns2 = new LinkedList<>();
// TODO: add proper units for type == 1. MILES and SECONDS PER MILE
columns2.add(new ActivitySummaryValue("", ActivitySummaryEntries.UNIT_NONE));
columns2.add(new ActivitySummaryValue(sample.getStrokes(), ActivitySummaryEntries.UNIT_STROKES));
columns2.add(new ActivitySummaryValue(sample.getAvgSwolf(), ActivitySummaryEntries.UNIT_NONE));
columns2.add(new ActivitySummaryValue(sample.getPace(), ActivitySummaryEntries.UNIT_NONE)); //TODO: seconds / 100 meters
segmentsTable.put("segments_table_" + tableIndex++,
new ActivitySummaryTableRowEntry(
ActivitySummaryEntries.GROUP_PACE,
columns2,
false,
false
)
);
segmentsTable.put("segments_table_" + tableIndex++,
new ActivitySummaryTableRowEntry(
ActivitySummaryEntries.GROUP_PACE,
new ArrayList<>(),
false,
false
)
);
}
if (!segmentsTable.isEmpty()) {
for (final Map.Entry e : segmentsTable.entrySet()) {
summaryData.add(e.getKey(), e.getValue());
}
}
}
if (unknownData) {
summaryData.add(
GBApplication.getContext().getString(R.string.unknownDataEncountered),
GBApplication.getContext().getString(R.string.yes).toUpperCase()
);
}
if (baseSummary.getName() == null) {
baseSummary.setName("Workout " + summary.getWorkoutNumber());
}
if (baseSummary.getGpxTrack() == null) {
baseSummary.setGpxTrack(summary.getGpxFileLocation());
}
// start time never changes
baseSummary.setEndTime(new Date(summary.getEndTimestamp() * 1000L));
baseSummary.setActivityKind(type.getCode());
baseSummary.setSummaryData(summaryData.toString());
} catch (Exception e) {
GB.toast("Exception parsing workout data", Toast.LENGTH_SHORT, GB.ERROR, e);
LOG.error("Exception parsing workout data", e);
}
}
}