[Huawei] Workout sync refactor

I hope it will fix #5523
This commit is contained in:
Martin.JM
2026-03-24 19:35:47 +01:00
committed by José Rebelo
parent 9eb3ed9026
commit 4ea8a4b77b
12 changed files with 1010 additions and 1336 deletions
@@ -337,7 +337,7 @@ public class Workout {
}
public static class WorkoutData {
public static final int id = 0x0a;
public static final byte id = 0x0a;
public static class Request extends HuaweiPacket {
@@ -712,7 +712,7 @@ public class Workout {
}
public static class WorkoutPace {
public static final int id = 0x0c;
public static final byte id = 0x0c;
public static class Request extends HuaweiPacket {
@@ -800,7 +800,7 @@ public class Workout {
}
public static class WorkoutSwimSegments {
public static final int id = 0x0e;
public static final byte id = 0x0e;
public static class Request extends HuaweiPacket {
@@ -901,7 +901,7 @@ public class Workout {
}
public static class WorkoutSpO2 {
public static final int id = 0x14;
public static final byte id = 0x14;
public static class Request extends HuaweiPacket {
@@ -1013,7 +1013,7 @@ public class Workout {
}
public static class WorkoutSections {
public static final int id = 0x16;
public static final byte id = 0x16;
public static class Request extends HuaweiPacket {
@@ -92,8 +92,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.ui.HuaweiStressCalibrationFragment;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiEcgDataSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiEcgDataSampleDao;
@@ -102,16 +100,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiEcgSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStageSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiSleepStatsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiStressSample;
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.HuaweiWorkoutSectionsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSectionsSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2Sample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummaryAdditionalValuesSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummaryAdditionalValuesSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSwimSegmentsSample;
@@ -170,7 +158,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetO
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSmartAlarmList;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWatchfaceParams;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWorkoutCapability;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWorkoutTotalsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendCameraRemoteSetupEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendCountryCodeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendDeviceReportThreshold;
@@ -204,7 +191,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetF
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetHiChainRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSleepDataCountRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetStepDataCountRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWorkoutCountRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetMusicRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.AlarmsRequest;
@@ -310,6 +296,8 @@ public class HuaweiSupportProvider {
protected HuaweiDataSyncManager huaweiDataSyncManager = new HuaweiDataSyncManager(this);
protected HuaweiWorkoutSyncManager huaweiWorkoutSyncManager = new HuaweiWorkoutSyncManager(this);
private HuaweiDataSyncFeatureManager huaweiDataSyncFeatureManager = null;
private HuaweiDataSyncNotificationPictures huaweiDataSyncNotificationPictures = null;
@@ -1653,80 +1641,25 @@ public class HuaweiSupportProvider {
if (!syncState.startWorkoutSync())
return;
int start = 0;
int end = (int) (System.currentTimeMillis() / 1000);
SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
long prefLastSyncTime = sharedPreferences.getLong("lastSportsActivityTimeMillis", 0);
if (prefLastSyncTime != 0) {
start = (int) (prefLastSyncTime / 1000);
// Reset for next calls
sharedPreferences.edit().putLong("lastSportsActivityTimeMillis", 0).apply();
} else {
try (DBHandler db = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(gbDevice, db.getDaoSession()).getId();
QueryBuilder<HuaweiWorkoutSummarySample> qb1 = db.getDaoSession().getHuaweiWorkoutSummarySampleDao().queryBuilder().where(
HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId),
HuaweiWorkoutSummarySampleDao.Properties.UserId.eq(userId)
).orderDesc(
HuaweiWorkoutSummarySampleDao.Properties.StartTimestamp
).limit(1);
List<HuaweiWorkoutSummarySample> samples1 = qb1.list();
if (!samples1.isEmpty())
start = samples1.get(0).getEndTimestamp();
QueryBuilder<BaseActivitySummary> qb2 = db.getDaoSession().getBaseActivitySummaryDao().queryBuilder().where(
BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId),
BaseActivitySummaryDao.Properties.UserId.eq(userId)
).orderDesc(
BaseActivitySummaryDao.Properties.StartTime
).limit(1);
List<BaseActivitySummary> samples2 = qb2.list();
if (!samples2.isEmpty())
start = Math.min(start, (int) (samples2.get(0).getEndTime().getTime() / 1000L));
start = start + 1;
} catch (Exception e) {
LOG.warn("Exception for getting start time, using 10/06/2022 - 00:00:00.");
huaweiWorkoutSyncManager.performSync(new HuaweiWorkoutSyncManager.WorkoutSyncCallback() {
@Override
public void syncComplete() {
LOG.info("Workout sync complete!");
syncState.stopWorkoutSync();
}
if (start == 0 || start == 1)
start = 1654819200;
}
final GetWorkoutCountRequest getWorkoutCountRequest;
if (isBLE()) {
nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder leBuilder = createLeTransactionBuilder("FetchWorkoutData");
leBuilder.setBusyTask(R.string.busy_task_fetch_activity_data);
getWorkoutCountRequest = new GetWorkoutCountRequest(this, leBuilder, start, end);
} else {
nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder brBuilder = createBrTransactionBuilder("FetchWorkoutData");
brBuilder.setBusyTask(R.string.busy_task_fetch_activity_data);
getWorkoutCountRequest = new GetWorkoutCountRequest(this, brBuilder, start, end);
}
getWorkoutCountRequest.setFinalizeReq(new RequestCallback() {
@Override
public void handleException(Request.ResponseParseException e) {
// This is propagated through the workout requests, hence the slightly generic error message
GB.toast(context, "Exception synchronizing workout", Toast.LENGTH_SHORT, GB.ERROR);
LOG.error("Exception synchronizing workout", e);
endOfWorkoutSync();
public void handleTimeout() {
LOG.error("Workout sync timeout!");
syncState.stopWorkoutSync();
}
@Override
public void handleException() {
LOG.error("Workout sync exception!");
syncState.stopWorkoutSync();
}
});
try {
getWorkoutCountRequest.doPerform();
} catch (IOException e) {
GB.toast(context, "Exception synchronizing workout", Toast.LENGTH_SHORT, GB.ERROR);
LOG.error("Error sending workout count - showing user that the workout sync failed", e);
endOfWorkoutSync();
}
}
public void onReset(int flags) {
@@ -2011,312 +1944,6 @@ public class HuaweiSupportProvider {
// TODO: potentially do more with this, maybe through realtime data?
}
public Long addWorkoutTotalsData(Workout.WorkoutTotals.Response packet) {
try (DBHandler db = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(gbDevice, db.getDaoSession()).getId();
// Avoid duplicates
QueryBuilder<HuaweiWorkoutSummarySample> qb = db.getDaoSession().getHuaweiWorkoutSummarySampleDao().queryBuilder().where(
HuaweiWorkoutSummarySampleDao.Properties.UserId.eq(userId),
HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId),
HuaweiWorkoutSummarySampleDao.Properties.WorkoutNumber.eq(packet.number),
HuaweiWorkoutSummarySampleDao.Properties.StartTimestamp.eq(packet.startTime),
HuaweiWorkoutSummarySampleDao.Properties.EndTimestamp.eq(packet.endTime)
);
List<HuaweiWorkoutSummarySample> results = qb.build().list();
Long workoutId = null;
if (!results.isEmpty())
workoutId = results.get(0).getWorkoutId();
byte[] raw;
if (packet.rawData == null)
raw = null;
else
raw = StringUtils.bytesToHex(packet.rawData).getBytes(StandardCharsets.UTF_8);
byte[] recoveryHeartRates;
if (packet.recoveryHeartRates == null)
recoveryHeartRates = null;
else
recoveryHeartRates = StringUtils.bytesToHex(packet.recoveryHeartRates).getBytes(StandardCharsets.UTF_8);
HuaweiWorkoutSummarySample summarySample = new HuaweiWorkoutSummarySample(
workoutId,
deviceId,
userId,
packet.number,
packet.status,
packet.startTime,
packet.endTime,
packet.calories,
packet.distance,
packet.stepCount,
packet.totalTime,
packet.duration,
packet.type,
packet.strokes,
packet.avgStrokeRate,
packet.poolLength,
packet.laps,
packet.avgSwolf,
raw,
null,
packet.maxAltitude,
packet.minAltitude,
packet.elevationGain,
packet.elevationLoss,
packet.workoutLoad,
packet.workoutAerobicEffect,
packet.workoutAnaerobicEffect,
packet.recoveryTime,
packet.minHeartRatePeak,
packet.maxHeartRatePeak,
recoveryHeartRates,
packet.swimType,
packet.maxMET,
packet.hrZoneType,
packet.runPaceZone1Min,
packet.runPaceZone2Min,
packet.runPaceZone3Min,
packet.runPaceZone4Min,
packet.runPaceZone5Min,
packet.runPaceZone5Max,
packet.runPaceZone1Time,
packet.runPaceZone2Time,
packet.runPaceZone3Time,
packet.runPaceZone4Time,
packet.runPaceZone5Time,
packet.algType,
packet.trainingPoints,
packet.longestStreak,
packet.tripped,
getDeviceState().isSupportsWorkoutNewSteps(),
null
);
db.getDaoSession().getHuaweiWorkoutSummarySampleDao().insertOrReplace(summarySample);
// We should completely replace values. Delete all and insert again.
final DeleteQuery<HuaweiWorkoutSummaryAdditionalValuesSample> tableDeleteQuery = db.getDaoSession().getHuaweiWorkoutSummaryAdditionalValuesSampleDao().queryBuilder()
.where(HuaweiWorkoutSummaryAdditionalValuesSampleDao.Properties.WorkoutId.eq(summarySample.getWorkoutId()))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
for (Map.Entry<String, String> entry : packet.additionalValues.entrySet()) {
HuaweiWorkoutSummaryAdditionalValuesSample summarySampleAdditionalValue = new HuaweiWorkoutSummaryAdditionalValuesSample(summarySample.getWorkoutId(), entry.getKey(), entry.getValue());
db.getDaoSession().getHuaweiWorkoutSummaryAdditionalValuesSampleDao().insertOrReplace(summarySampleAdditionalValue);
}
return summarySample.getWorkoutId();
} catch (Exception e) {
LOG.error("Failed to add workout totals data to database", e);
return null;
}
}
public void addWorkoutSampleData(Long workoutId, List<Workout.WorkoutData.Response.Data> dataList) {
if (workoutId == null)
return;
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutDataSampleDao dao = db.getDaoSession().getHuaweiWorkoutDataSampleDao();
for (Workout.WorkoutData.Response.Data data : dataList) {
byte[] unknown;
if (data.unknownData == null)
unknown = null;
else
unknown = StringUtils.bytesToHex(data.unknownData).getBytes(StandardCharsets.UTF_8);
HuaweiWorkoutDataSample dataSample = new HuaweiWorkoutDataSample(
workoutId,
data.timestamp,
data.heartRate,
data.speed,
data.stepRate,
data.cadence,
data.stepLength,
data.groundContactTime,
data.impact,
data.swingAngle,
data.foreFootLanding,
data.midFootLanding,
data.backFootLanding,
data.eversionAngle,
data.swolf,
data.strokeRate,
unknown,
data.calories,
data.cyclingPower,
data.frequency,
data.altitude,
data.hangTime,
data.impactHangRate,
data.rideCadence,
data.ap,
data.vo,
data.gtb,
data.vr,
data.ceiling,
data.temp,
data.spo2,
data.cns
);
dao.insertOrReplace(dataSample);
}
} catch (Exception e) {
LOG.error("Failed to add workout data to database", e);
}
}
public void addWorkoutPaceData(Long workoutId, List<Workout.WorkoutPace.Response.Block> paceList, short number) {
if (workoutId == null)
return;
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutPaceSampleDao dao = db.getDaoSession().getHuaweiWorkoutPaceSampleDao();
if (number == 0) {
final DeleteQuery<HuaweiWorkoutPaceSample> tableDeleteQuery = dao.queryBuilder()
.where(HuaweiWorkoutPaceSampleDao.Properties.WorkoutId.eq(workoutId))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
}
int paceIndex = (int) dao.queryBuilder().where(HuaweiWorkoutPaceSampleDao.Properties.WorkoutId.eq(workoutId)).count();
for (Workout.WorkoutPace.Response.Block block : paceList) {
Integer correction = block.hasCorrection ? (int) block.correction : null;
HuaweiWorkoutPaceSample paceSample = new HuaweiWorkoutPaceSample(
workoutId,
paceIndex++,
block.distance,
block.type,
block.pace,
block.pointIndex,
correction
);
dao.insertOrReplace(paceSample);
}
} catch (Exception e) {
LOG.error("Failed to add workout pace data to database", e);
}
}
public void addWorkoutSwimSegmentsData(Long workoutId, List<Workout.WorkoutSwimSegments.Response.Block> paceList, short number) {
if (workoutId == null)
return;
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutSwimSegmentsSampleDao dao = db.getDaoSession().getHuaweiWorkoutSwimSegmentsSampleDao();
if (number == 0) {
final DeleteQuery<HuaweiWorkoutSwimSegmentsSample> tableDeleteQuery = dao.queryBuilder()
.where(HuaweiWorkoutSwimSegmentsSampleDao.Properties.WorkoutId.eq(workoutId))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
}
int paceIndex = (int) dao.queryBuilder().where(HuaweiWorkoutSwimSegmentsSampleDao.Properties.WorkoutId.eq(workoutId)).count();
for (Workout.WorkoutSwimSegments.Response.Block block : paceList) {
HuaweiWorkoutSwimSegmentsSample swimSectionSample = new HuaweiWorkoutSwimSegmentsSample(
workoutId,
paceIndex++,
block.distance,
block.type,
block.pace,
block.pointIndex,
block.segment,
block.swimType,
block.strokes,
block.avgSwolf,
block.time
);
dao.insertOrReplace(swimSectionSample);
}
} catch (Exception e) {
LOG.error("Failed to add workout swim section data to database", e);
}
}
public void addWorkoutSpO2Data(Long workoutId, List<Workout.WorkoutSpO2.Response.Block> spO2List, short number) {
if (workoutId == null)
return;
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutSpO2SampleDao dao = db.getDaoSession().getHuaweiWorkoutSpO2SampleDao();
if (number == 0) {
final DeleteQuery<HuaweiWorkoutSpO2Sample> tableDeleteQuery = dao.queryBuilder()
.where(HuaweiWorkoutSpO2SampleDao.Properties.WorkoutId.eq(workoutId))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
}
for (Workout.WorkoutSpO2.Response.Block block : spO2List) {
HuaweiWorkoutSpO2Sample spO2Sample = new HuaweiWorkoutSpO2Sample(
workoutId,
block.interval,
block.value
);
dao.insertOrReplace(spO2Sample);
}
} catch (Exception e) {
LOG.error("Failed to add workout SpO2 data to database", e);
}
}
public void addWorkoutSectionsData(Long workoutId, List<Workout.WorkoutSections.Response.Block> spO2List, short number) {
if (workoutId == null)
return;
// NOTE: All fields of this data is optional. At this point I don't all workouts that this data used.
// I decided to add two additional fields dataIdx and rowIdx as primary keys that should identify each row
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutSectionsSampleDao dao = db.getDaoSession().getHuaweiWorkoutSectionsSampleDao();
if (number == 0) {
final DeleteQuery<HuaweiWorkoutSectionsSample> tableDeleteQuery = dao.queryBuilder()
.where(HuaweiWorkoutSectionsSampleDao.Properties.WorkoutId.eq(workoutId))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
}
int i = 0;
for (Workout.WorkoutSections.Response.Block block : spO2List) {
HuaweiWorkoutSectionsSample huaweiWorkoutSectionsSample = new HuaweiWorkoutSectionsSample(
workoutId,
number,
i++,
block.num,
block.time,
block.distance,
block.pace,
block.heartRate,
block.cadence,
block.stepLength,
block.totalRise,
block.totalDescend,
block.groundContactTime,
block.groundImpact,
block.swingAngle,
block.eversion,
block.avgCadence,
block.divingUnderwaterTime,
block.divingMaxDepth,
block.divingUnderwaterTime,
block.divingBreakTime
);
dao.insertOrReplace(huaweiWorkoutSectionsSample);
}
} catch (Exception e) {
LOG.error("Failed to add workout sections data to database", e);
}
}
public void setWearLocation() {
try {
SetWearLocationRequest setWearLocationReq = new SetWearLocationRequest(this);
@@ -3245,29 +2872,6 @@ public class HuaweiSupportProvider {
return true;
}
public void nextWorkoutSync(List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder, RequestCallback finalizeReq) {
if (!remainder.isEmpty()) {
GetWorkoutTotalsRequest nextRequest = new GetWorkoutTotalsRequest(
this,
remainder.remove(0),
remainder
);
nextRequest.setFinalizeReq(finalizeReq);
// Cannot do this with nextRequest because it's in a callback
try {
nextRequest.doPerform();
} catch (IOException e) {
finalizeReq.handleException(new Request.ResponseParseException("Cannot send next request", e));
}
} else {
this.endOfWorkoutSync();
}
}
public void endOfWorkoutSync() {
this.syncState.stopWorkoutSync();
}
public void downloadWorkoutPdrFiles(short workoutId, Long databaseId) {
if(!getDeviceState().isSupportsGpsNewSync())
return;
@@ -6,12 +6,12 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
class HuaweiSyncState {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiSyncState.class);
private final HuaweiSupportProvider supportProvider;
private final List<Integer> syncQueue = new ArrayList<>(2);
@@ -134,6 +134,7 @@ class HuaweiSyncState {
return false;
}
this.workoutSync = true;
this.supportProvider.getDevice().setBusyTask(R.string.busy_task_fetch_activity_data, supportProvider.getContext());
}
LOG.debug("Set workout sync state to true");
return true;
@@ -0,0 +1,979 @@
/* Copyright (C) 2026 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
import android.content.SharedPreferences;
import android.widget.Toast;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import de.greenrobot.dao.query.DeleteQuery;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
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.HuaweiWorkoutSectionsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSectionsSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2Sample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummaryAdditionalValuesSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummaryAdditionalValuesSampleDao;
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.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.RequestBuilder;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class HuaweiWorkoutSyncManager {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWorkoutSyncManager.class);
/*
* TODO: We add the workout totals first, if at any point after that a sync fails, the workout
* will not be synchronised again automatically. Might want to fix that.
*/
/*
* General dataflow in this class:
* - Sync function sends a request to the device
* - Response gets into handle function, which calls the next sync function
*/
private record WorkoutSyncRequest(int start, int end) { }
public interface WorkoutSyncCallback {
void syncComplete();
void handleTimeout();
void handleException();
}
private final HuaweiSupportProvider supportProvider;
private final ArrayList<WorkoutSyncRequest> syncRequests;
private WorkoutSyncRequest currentSyncRequest;
/// Used for the callback, but also to see if there's still a sync running
private WorkoutSyncCallback callback = null;
private List<Workout.WorkoutCount.Response.WorkoutNumbers> workoutNumbersList;
private Workout.WorkoutCount.Response.WorkoutNumbers currentWorkoutNumbers;
private Long currentDatabaseId;
private int currentNumber; // Used for data, pace, etc...
public HuaweiWorkoutSyncManager(HuaweiSupportProvider supportProvider) {
this.supportProvider = supportProvider;
this.syncRequests = new ArrayList<>();
}
public void performSync(WorkoutSyncCallback callback) {
int start = getStartTimeFromDb();
int end = (int) (System.currentTimeMillis() / 1000);
WorkoutSyncRequest syncRequest = new WorkoutSyncRequest(start, end);
synchronized (supportProvider) {
if (this.callback != null) {
callback.handleException();
return;
}
this.callback = callback;
syncRequests.add(syncRequest);
}
syncNextFromQueue();
}
private void resetState() {
synchronized (this.supportProvider) {
this.callback = null;
if (!this.syncRequests.isEmpty())
LOG.error("SyncRequests is not empty yet state is reset");
this.syncRequests.clear();
this.workoutNumbersList = null;
this.currentDatabaseId = null;
}
}
private void handleException(@Nullable Exception e) {
GB.toast("Exception synchronizing workout", Toast.LENGTH_SHORT, GB.ERROR, e);
this.callback.handleException();
resetState();
}
private void handleTimeout() {
GB.toast("Timeout when synchronizing workout", Toast.LENGTH_SHORT, GB.WARN);
this.callback.handleTimeout();
resetState();
}
private void syncNextFromQueue() {
if (this.syncRequests.isEmpty()) {
this.callback.syncComplete();
resetState();
return;
}
this.currentSyncRequest = this.syncRequests.remove(0);
Request getWorkoutCountRequest = new RequestBuilder<Workout.WorkoutCount.Response>(
this.supportProvider,
Workout.id,
Workout.WorkoutCount.id,
new Workout.WorkoutCount.Request(
supportProvider.getParamsProvider(),
this.currentSyncRequest.start,
this.currentSyncRequest.end
)
)
.setTimeout(5000) // Set a 5 second timeout
.onCallback(this::handleWorkoutCount)
.onException(e -> {
LOG.error("Exception receiving workout count", e);
handleException(e);
})
.onTimeout(packet -> {
LOG.error("Timeout on workout count");
handleTimeout();
})
.build();
try {
getWorkoutCountRequest.doPerform();
} catch (IOException e) {
LOG.error("Exception sending workout count", e);
handleException(e);
}
}
private void handleWorkoutCount(Workout.WorkoutCount.Response packet) {
if (packet.error != null) {
LOG.warn("Error when retrieving/parsing workout count: {}", packet.error);
handleException(null);
return;
}
if (packet.count == 0 || packet.workoutNumbers == null) {
this.callback.syncComplete();
resetState();
return;
}
if (packet.count != packet.workoutNumbers.size()) {
// If it errors, try to get less data - half the timeframe
int start = this.currentSyncRequest.start;
int end = this.currentSyncRequest.end;
int half = start + (end - start) / 2;
this.syncRequests.add(new WorkoutSyncRequest(start, half));
this.syncRequests.add(new WorkoutSyncRequest(half + 1, end));
syncNextFromQueue();
return;
}
this.workoutNumbersList = packet.workoutNumbers;
// Has to be sorted for the timestamp-based sync stat that we use
this.workoutNumbersList.sort(Comparator.comparingInt(o -> o.workoutNumber));
syncNextWorkout();
}
private void syncNextWorkout() {
if (this.workoutNumbersList == null || this.workoutNumbersList.isEmpty()) {
syncNextFromQueue();
return;
}
this.currentWorkoutNumbers = this.workoutNumbersList.remove(0);
Request getWorkoutTotalsRequest = new RequestBuilder<Workout.WorkoutTotals.Response>(
this.supportProvider,
Workout.id,
Workout.WorkoutTotals.id,
new Workout.WorkoutTotals.Request(
this.supportProvider.getParamsProvider(),
this.currentWorkoutNumbers.workoutNumber
)
)
.setTimeout(5000)
.onCallback(this::handleWorkoutTotals)
.onException(e -> {
LOG.error("Exception receiving workout totals", e);
handleException(e);
})
.onTimeout(packet -> {
LOG.error("Timeout on workout totals");
handleTimeout();
})
.build();
try {
getWorkoutTotalsRequest.doPerform();
} catch (IOException e) {
LOG.error("Exception sending workout totals", e);
handleException(e);
}
}
private void handleWorkoutTotals(Workout.WorkoutTotals.Response packet) {
if (packet.error != null) {
LOG.error("Error {} occurred during workout totals sync", packet.error);
handleException(null);
// TODO: Original went to the next workout here. We might still need to do that.
return;
}
if (packet.number != this.currentWorkoutNumbers.workoutNumber) {
LOG.error("Incorrect workout number for totals response!");
handleException(null);
return;
}
LOG.info("Workout {} totals:", this.currentWorkoutNumbers.workoutNumber);
LOG.info("Number : {}", packet.number);
LOG.info("Status : {}", packet.status);
LOG.info("Start : {}", packet.startTime);
LOG.info("End : {}", packet.endTime);
LOG.info("Calories: {}", packet.calories);
LOG.info("Distance: {}", packet.distance);
LOG.info("Steps : {}", packet.stepCount);
LOG.info("Time : {}", packet.totalTime);
LOG.info("Duration: {}", packet.duration);
LOG.info("Type : {}", packet.type);
this.currentDatabaseId = addWorkoutTotalsDataToDb(packet);
if (this.currentDatabaseId == null) {
handleException(null);
return;
}
this.currentNumber = 0;
syncData();
}
private void syncData() {
if (this.currentNumber >= this.currentWorkoutNumbers.dataCount) {
this.currentNumber = 0;
syncPace();
return;
}
Request getWorkoutDataRequest = new RequestBuilder<Workout.WorkoutData.Response>(
this.supportProvider,
Workout.id,
Workout.WorkoutData.id,
new Workout.WorkoutData.Request(
this.supportProvider.getParamsProvider(),
this.currentWorkoutNumbers.workoutNumber,
(short) this.currentNumber,
this.supportProvider.getDeviceState().isSupportsWorkoutNewSteps()
)
)
.setTimeout(5000)
.onCallback(this::handleData)
.onException(e -> {
LOG.error("Exception receiving workout data", e);
handleException(e);
})
.onTimeout(packet -> {
LOG.error("Timeout on workout data");
handleTimeout();
})
.build();
try {
getWorkoutDataRequest.doPerform();
} catch (IOException e) {
LOG.error("Exception sending workout data request", e);
handleException(e);
}
}
private void handleData(Workout.WorkoutData.Response packet) {
// TODO: Original didn't even have this check...
if (packet.error != null) {
LOG.error("Error {} occurred during workout data sync", packet.error);
handleException(null);
// TODO: Original went to the next workout here. We might still need to do that.
return;
}
if (packet.workoutNumber != this.currentWorkoutNumbers.workoutNumber) {
LOG.error("Incorrect workout number for data response!");
handleException(null);
return;
}
if (packet.dataNumber != this.currentNumber) {
LOG.error("Incorrect data number!");
handleException(null);
return;
}
LOG.info("Workout {} data {}:", this.currentWorkoutNumbers.workoutNumber, this.currentNumber);
LOG.info("Workout : {}", packet.workoutNumber);
LOG.info("Data num: {}", packet.dataNumber);
LOG.info("Header : {}", Arrays.toString(packet.rawHeader));
LOG.info("Header : {}", packet.header);
LOG.info("Data : {}", Arrays.toString(packet.rawData));
LOG.info("Data : {}", Arrays.toString(packet.dataList.toArray()));
LOG.info("Bitmap : {}", packet.innerBitmap);
addWorkoutSampleDataToDb(this.currentDatabaseId, packet.dataList);
this.currentNumber += 1;
syncData();
}
private void syncPace() {
if (this.currentNumber >= this.currentWorkoutNumbers.paceCount) {
this.currentNumber = 0;
syncSwimSegments();
return;
}
Request getWorkoutPaceRequest = new RequestBuilder<Workout.WorkoutPace.Response>(
this.supportProvider,
Workout.id,
Workout.WorkoutPace.id,
new Workout.WorkoutPace.Request(
this.supportProvider.getParamsProvider(),
this.currentWorkoutNumbers.workoutNumber,
(short) this.currentNumber
)
)
.setTimeout(5000)
.onCallback(this::handlePace)
.onException(e -> {
LOG.error("Exception receiving workout pace", e);
handleException(e);
})
.onTimeout(packet -> {
LOG.error("Tiemout on workout pace");
handleTimeout();
})
.build();
try {
getWorkoutPaceRequest.doPerform();
} catch (IOException e) {
LOG.error("Exception sending workout data request", e);
handleException(e);
}
}
private void handlePace(Workout.WorkoutPace.Response packet) {
if (packet.error != null) {
LOG.error("Error {} occurred during workout pace sync", packet.error);
handleException(null);
// TODO: Original went to the next workout here. We might still need to do that.
return;
}
if (packet.workoutNumber != this.currentWorkoutNumbers.workoutNumber) {
LOG.error("Incorrect workout number for pace response!");
handleException(null);
return;
}
if (packet.paceNumber != currentNumber) {
LOG.error("Incorrect pace number!");
handleException(null);
return;
}
LOG.info("Workout {} pace {}:", this.currentWorkoutNumbers.workoutNumber, this.currentNumber);
LOG.info("Workout : {}", packet.workoutNumber);
LOG.info("Pace : {}", packet.paceNumber);
LOG.info("Block num: {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
addWorkoutPaceDataToDb(this.currentDatabaseId, packet.blocks, packet.paceNumber);
currentNumber += 1;
syncPace();
}
private void syncSwimSegments() {
if (this.currentNumber >= this.currentWorkoutNumbers.segmentsCount) {
this.currentNumber = 0;
syncSpO2();
return;
}
Request getWorkoutSwimSegmentsRequest = new RequestBuilder<Workout.WorkoutSwimSegments.Response>(
this.supportProvider,
Workout.id,
Workout.WorkoutSwimSegments.id,
new Workout.WorkoutSwimSegments.Request(
this.supportProvider.getParamsProvider(),
this.currentWorkoutNumbers.workoutNumber,
(short) this.currentNumber
)
)
.setTimeout(5000)
.onCallback(this::handleSwimSegment)
.onException(e -> {
LOG.error("Exception receiving workout swim segments", e);
handleException(e);
})
.onTimeout(packet -> {
LOG.error("Timeout on workout swim segments");
handleTimeout();
})
.build();
try {
getWorkoutSwimSegmentsRequest.doPerform();
} catch (IOException e) {
LOG.error("Exception sending workout swim segment request", e);
handleException(e);
}
}
private void handleSwimSegment(Workout.WorkoutSwimSegments.Response packet) {
if (packet.error != null) {
LOG.error("Error {} occurred during workout swim segments sync", packet.error);
handleException(null);
// TODO: Original went to the next workout here. We might still need to do that.
return;
}
if (packet.workoutNumber != this.currentWorkoutNumbers.workoutNumber) {
LOG.error("Incorrect workout number for swim segment response!");
handleException(null);
return;
}
if (packet.segmentNumber != this.currentNumber) {
LOG.error("Incorrect swim segment number!");
handleException(null);
return;
}
LOG.info("Workout {} segment {}:", this.currentWorkoutNumbers.workoutNumber, this.currentNumber);
LOG.info("Workout : {}", packet.workoutNumber);
LOG.info("Segments : {}", packet.segmentNumber);
LOG.info("Block num: {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
addWorkoutSwimSegmentsDataToDb(this.currentDatabaseId, packet.blocks, packet.segmentNumber);
this.currentNumber += 1;
syncSwimSegments();
}
private void syncSpO2() {
if (this.currentNumber >= this.currentWorkoutNumbers.spO2Count) {
this.currentNumber = 0;
syncSections();
return;
}
Request getWorkoutSpO2Request = new RequestBuilder<Workout.WorkoutSpO2.Response>(
this.supportProvider,
Workout.id,
Workout.WorkoutSpO2.id,
new Workout.WorkoutSpO2.Request(
this.supportProvider.getParamsProvider(),
this.currentWorkoutNumbers.workoutNumber,
(short) this.currentNumber
)
)
.setTimeout(5000)
.onCallback(this::handleSpO2)
.onException(e -> {
LOG.error("Exception receiving SpO2 data");
handleException(e);
})
.onTimeout(packet -> {
LOG.error("Timeout on SpO2 data");
handleTimeout();
})
.build();
try {
getWorkoutSpO2Request.doPerform();
} catch (IOException e) {
LOG.error("Exception sending workout SpO2 request", e);
handleException(e);
}
}
private void handleSpO2(Workout.WorkoutSpO2.Response packet) {
if (packet.error != null) {
LOG.error("Error {} occurred during workout SpO2 sync", packet.error);
handleException(null);
// TODO: Original went to the next workout here. We might still need to do that.
return;
}
LOG.info("Workout {} current {}:", this.currentWorkoutNumbers.workoutNumber, this.currentNumber);
LOG.info("spO2Number1: {}", packet.spO2Number1);
LOG.info("spO2Number2: {}", packet.spO2Number2);
LOG.info("Block num : {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
addWorkoutSpO2DataToDb(this.currentDatabaseId, packet.blocks, (short) this.currentNumber);
this.currentNumber += 1;
syncSpO2();
}
private void syncSections() {
if (this.currentNumber >= this.currentWorkoutNumbers.sectionsCount) {
this.currentNumber = 0;
finishSingleWorkoutSync();
return;
}
Request getWorkoutSectionsRequest = new RequestBuilder<Workout.WorkoutSections.Response>(
this.supportProvider,
Workout.id,
Workout.WorkoutSections.id,
new Workout.WorkoutSections.Request(
this.supportProvider.getParamsProvider(),
this.currentWorkoutNumbers.workoutNumber,
(short) this.currentNumber
)
)
.setTimeout(5000)
.onCallback(this::handleSections)
.onException(e -> {
LOG.error("Exception receiving workout sections", e);
handleException(e);
})
.onTimeout(packet -> {
LOG.error("Timeout on workout sections");
handleTimeout();
})
.build();
try {
getWorkoutSectionsRequest.doPerform();
} catch (IOException e) {
LOG.error("Exception sending workout sections request", e);
handleException(e);
}
}
private void handleSections(Workout.WorkoutSections.Response packet) {
if (packet.error != null) {
if (packet.error == 0x0001E079) {
LOG.warn("Error 0001E079 occurred during workout swim segments sync. This seems to be normal, so it's ignored.");
this.currentNumber += 1;
syncSections();
return;
}
LOG.error("Error {} occurred during workout sections sync", packet.error);
handleException(null);
// TODO: Original went to the next workout here. We might still need to do that.
return;
}
LOG.info("Workout {} section {}:", this.currentWorkoutNumbers.workoutNumber, this.currentNumber);
LOG.info("workoutId : {}", packet.workoutId);
LOG.info("number : {}", packet.number);
LOG.info("Block num : {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
addWorkoutSectionsDataToDb(this.currentDatabaseId, packet.blocks, (short) this.currentNumber);
currentNumber += 1;
syncSections();
}
private void finishSingleWorkoutSync() {
new HuaweiWorkoutGbParser(
this.supportProvider.getDevice(),
this.supportProvider.getContext()
).parseWorkout(this.currentDatabaseId);
supportProvider.downloadWorkoutGpsFiles(
this.currentWorkoutNumbers.workoutNumber,
this.currentDatabaseId,
this::syncNextWorkout
);
}
/*
--------------------------------------------------
DATABASE FUNCTIONS
--------------------------------------------------
*/
private int getStartTimeFromDb() {
int start = 0;
GBDevice gbDevice = supportProvider.getDevice();
SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
long prefLastSyncTime = sharedPreferences.getLong("lastSportsActivityTimeMillis", 0);
if (prefLastSyncTime != 0) {
start = (int) (prefLastSyncTime / 1000);
// Reset for next calls
sharedPreferences.edit().putLong("lastSportsActivityTimeMillis", 0).apply();
} else {
try (DBHandler db = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(gbDevice, db.getDaoSession()).getId();
QueryBuilder<HuaweiWorkoutSummarySample> qb1 = db.getDaoSession().getHuaweiWorkoutSummarySampleDao().queryBuilder().where(
HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId),
HuaweiWorkoutSummarySampleDao.Properties.UserId.eq(userId)
).orderDesc(
HuaweiWorkoutSummarySampleDao.Properties.StartTimestamp
).limit(1);
List<HuaweiWorkoutSummarySample> samples1 = qb1.list();
if (!samples1.isEmpty())
start = samples1.get(0).getEndTimestamp();
QueryBuilder<BaseActivitySummary> qb2 = db.getDaoSession().getBaseActivitySummaryDao().queryBuilder().where(
BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId),
BaseActivitySummaryDao.Properties.UserId.eq(userId)
).orderDesc(
BaseActivitySummaryDao.Properties.StartTime
).limit(1);
List<BaseActivitySummary> samples2 = qb2.list();
if (!samples2.isEmpty())
start = Math.min(start, (int) (samples2.get(0).getEndTime().getTime() / 1000L));
start = start + 1;
} catch (Exception e) {
LOG.warn("Exception for getting start time, using 10/06/2022 - 00:00:00.");
start = 1654819200;
}
if (start == 0 || start == 1)
start = 1654819200;
}
return start;
}
public Long addWorkoutTotalsDataToDb(Workout.WorkoutTotals.Response packet) {
try (DBHandler db = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(
this.supportProvider.getDevice(),
db.getDaoSession()
).getId();
// Avoid duplicates
QueryBuilder<HuaweiWorkoutSummarySample> qb = db.getDaoSession().getHuaweiWorkoutSummarySampleDao().queryBuilder().where(
HuaweiWorkoutSummarySampleDao.Properties.UserId.eq(userId),
HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId),
HuaweiWorkoutSummarySampleDao.Properties.WorkoutNumber.eq(packet.number),
HuaweiWorkoutSummarySampleDao.Properties.StartTimestamp.eq(packet.startTime),
HuaweiWorkoutSummarySampleDao.Properties.EndTimestamp.eq(packet.endTime)
);
List<HuaweiWorkoutSummarySample> results = qb.build().list();
Long workoutId = null;
if (!results.isEmpty())
workoutId = results.get(0).getWorkoutId();
byte[] raw;
if (packet.rawData == null)
raw = null;
else
raw = StringUtils.bytesToHex(packet.rawData).getBytes(StandardCharsets.UTF_8);
byte[] recoveryHeartRates;
if (packet.recoveryHeartRates == null)
recoveryHeartRates = null;
else
recoveryHeartRates = StringUtils.bytesToHex(packet.recoveryHeartRates).getBytes(StandardCharsets.UTF_8);
HuaweiWorkoutSummarySample summarySample = new HuaweiWorkoutSummarySample(
workoutId,
deviceId,
userId,
packet.number,
packet.status,
packet.startTime,
packet.endTime,
packet.calories,
packet.distance,
packet.stepCount,
packet.totalTime,
packet.duration,
packet.type,
packet.strokes,
packet.avgStrokeRate,
packet.poolLength,
packet.laps,
packet.avgSwolf,
raw,
null,
packet.maxAltitude,
packet.minAltitude,
packet.elevationGain,
packet.elevationLoss,
packet.workoutLoad,
packet.workoutAerobicEffect,
packet.workoutAnaerobicEffect,
packet.recoveryTime,
packet.minHeartRatePeak,
packet.maxHeartRatePeak,
recoveryHeartRates,
packet.swimType,
packet.maxMET,
packet.hrZoneType,
packet.runPaceZone1Min,
packet.runPaceZone2Min,
packet.runPaceZone3Min,
packet.runPaceZone4Min,
packet.runPaceZone5Min,
packet.runPaceZone5Max,
packet.runPaceZone1Time,
packet.runPaceZone2Time,
packet.runPaceZone3Time,
packet.runPaceZone4Time,
packet.runPaceZone5Time,
packet.algType,
packet.trainingPoints,
packet.longestStreak,
packet.tripped,
this.supportProvider.getDeviceState().isSupportsWorkoutNewSteps(),
null
);
db.getDaoSession().getHuaweiWorkoutSummarySampleDao().insertOrReplace(summarySample);
// We should completely replace values. Delete all and insert again.
final DeleteQuery<HuaweiWorkoutSummaryAdditionalValuesSample> tableDeleteQuery = db.getDaoSession().getHuaweiWorkoutSummaryAdditionalValuesSampleDao().queryBuilder()
.where(HuaweiWorkoutSummaryAdditionalValuesSampleDao.Properties.WorkoutId.eq(summarySample.getWorkoutId()))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
for (Map.Entry<String, String> entry : packet.additionalValues.entrySet()) {
HuaweiWorkoutSummaryAdditionalValuesSample summarySampleAdditionalValue = new HuaweiWorkoutSummaryAdditionalValuesSample(summarySample.getWorkoutId(), entry.getKey(), entry.getValue());
db.getDaoSession().getHuaweiWorkoutSummaryAdditionalValuesSampleDao().insertOrReplace(summarySampleAdditionalValue);
}
return summarySample.getWorkoutId();
} catch (Exception e) {
LOG.error("Failed to add workout totals data to database", e);
return null;
}
}
public void addWorkoutSampleDataToDb(Long workoutId, List<Workout.WorkoutData.Response.Data> dataList) {
if (workoutId == null)
return;
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutDataSampleDao dao = db.getDaoSession().getHuaweiWorkoutDataSampleDao();
for (Workout.WorkoutData.Response.Data data : dataList) {
byte[] unknown;
if (data.unknownData == null)
unknown = null;
else
unknown = StringUtils.bytesToHex(data.unknownData).getBytes(StandardCharsets.UTF_8);
HuaweiWorkoutDataSample dataSample = new HuaweiWorkoutDataSample(
workoutId,
data.timestamp,
data.heartRate,
data.speed,
data.stepRate,
data.cadence,
data.stepLength,
data.groundContactTime,
data.impact,
data.swingAngle,
data.foreFootLanding,
data.midFootLanding,
data.backFootLanding,
data.eversionAngle,
data.swolf,
data.strokeRate,
unknown,
data.calories,
data.cyclingPower,
data.frequency,
data.altitude,
data.hangTime,
data.impactHangRate,
data.rideCadence,
data.ap,
data.vo,
data.gtb,
data.vr,
data.ceiling,
data.temp,
data.spo2,
data.cns
);
dao.insertOrReplace(dataSample);
}
} catch (Exception e) {
LOG.error("Failed to add workout data to database", e);
}
}
public void addWorkoutPaceDataToDb(Long workoutId, List<Workout.WorkoutPace.Response.Block> paceList, short number) {
if (workoutId == null)
return;
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutPaceSampleDao dao = db.getDaoSession().getHuaweiWorkoutPaceSampleDao();
if (number == 0) {
final DeleteQuery<HuaweiWorkoutPaceSample> tableDeleteQuery = dao.queryBuilder()
.where(HuaweiWorkoutPaceSampleDao.Properties.WorkoutId.eq(workoutId))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
}
int paceIndex = (int) dao.queryBuilder().where(HuaweiWorkoutPaceSampleDao.Properties.WorkoutId.eq(workoutId)).count();
for (Workout.WorkoutPace.Response.Block block : paceList) {
Integer correction = block.hasCorrection ? (int) block.correction : null;
HuaweiWorkoutPaceSample paceSample = new HuaweiWorkoutPaceSample(
workoutId,
paceIndex++,
block.distance,
block.type,
block.pace,
block.pointIndex,
correction
);
dao.insertOrReplace(paceSample);
}
} catch (Exception e) {
LOG.error("Failed to add workout pace data to database", e);
}
}
public void addWorkoutSwimSegmentsDataToDb(Long workoutId, List<Workout.WorkoutSwimSegments.Response.Block> paceList, short number) {
if (workoutId == null)
return;
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutSwimSegmentsSampleDao dao = db.getDaoSession().getHuaweiWorkoutSwimSegmentsSampleDao();
if (number == 0) {
final DeleteQuery<HuaweiWorkoutSwimSegmentsSample> tableDeleteQuery = dao.queryBuilder()
.where(HuaweiWorkoutSwimSegmentsSampleDao.Properties.WorkoutId.eq(workoutId))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
}
int paceIndex = (int) dao.queryBuilder().where(HuaweiWorkoutSwimSegmentsSampleDao.Properties.WorkoutId.eq(workoutId)).count();
for (Workout.WorkoutSwimSegments.Response.Block block : paceList) {
HuaweiWorkoutSwimSegmentsSample swimSectionSample = new HuaweiWorkoutSwimSegmentsSample(
workoutId,
paceIndex++,
block.distance,
block.type,
block.pace,
block.pointIndex,
block.segment,
block.swimType,
block.strokes,
block.avgSwolf,
block.time
);
dao.insertOrReplace(swimSectionSample);
}
} catch (Exception e) {
LOG.error("Failed to add workout swim section data to database", e);
}
}
public void addWorkoutSpO2DataToDb(Long workoutId, List<Workout.WorkoutSpO2.Response.Block> spO2List, short number) {
if (workoutId == null)
return;
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutSpO2SampleDao dao = db.getDaoSession().getHuaweiWorkoutSpO2SampleDao();
if (number == 0) {
final DeleteQuery<HuaweiWorkoutSpO2Sample> tableDeleteQuery = dao.queryBuilder()
.where(HuaweiWorkoutSpO2SampleDao.Properties.WorkoutId.eq(workoutId))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
}
for (Workout.WorkoutSpO2.Response.Block block : spO2List) {
HuaweiWorkoutSpO2Sample spO2Sample = new HuaweiWorkoutSpO2Sample(
workoutId,
block.interval,
block.value
);
dao.insertOrReplace(spO2Sample);
}
} catch (Exception e) {
LOG.error("Failed to add workout SpO2 data to database", e);
}
}
public void addWorkoutSectionsDataToDb(Long workoutId, List<Workout.WorkoutSections.Response.Block> sectionsList, short number) {
if (workoutId == null)
return;
// NOTE: All fields of this data is optional. At this point I don't all workouts that this data used.
// I decided to add two additional fields dataIdx and rowIdx as primary keys that should identify each row
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutSectionsSampleDao dao = db.getDaoSession().getHuaweiWorkoutSectionsSampleDao();
if (number == 0) {
final DeleteQuery<HuaweiWorkoutSectionsSample> tableDeleteQuery = dao.queryBuilder()
.where(HuaweiWorkoutSectionsSampleDao.Properties.WorkoutId.eq(workoutId))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
}
int i = 0;
for (Workout.WorkoutSections.Response.Block block : sectionsList) {
HuaweiWorkoutSectionsSample huaweiWorkoutSectionsSample = new HuaweiWorkoutSectionsSample(
workoutId,
number,
i++,
block.num,
block.time,
block.distance,
block.pace,
block.heartRate,
block.cadence,
block.stepLength,
block.totalRise,
block.totalDescend,
block.groundContactTime,
block.groundImpact,
block.swingAngle,
block.eversion,
block.avgCadence,
block.divingUnderwaterTime,
block.divingMaxDepth,
block.divingUnderwaterTime,
block.divingBreakTime
);
dao.insertOrReplace(huaweiWorkoutSectionsSample);
}
} catch (Exception e) {
LOG.error("Failed to add workout sections data to database", e);
}
}
}
@@ -1,118 +0,0 @@
/* 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Comparator;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class GetWorkoutCountRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutCountRequest.class);
private final int start;
private final int end;
public GetWorkoutCountRequest(
HuaweiSupportProvider support,
nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder,
int start,
int end
) {
super(support, builder);
this.serviceId = Workout.id;
this.commandId = Workout.WorkoutCount.id;
this.start = start;
this.end = end;
}
public GetWorkoutCountRequest(
HuaweiSupportProvider support,
nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder builder,
int start,
int end
) {
super(support, builder);
this.serviceId = Workout.id;
this.commandId = Workout.WorkoutCount.id;
this.start = start;
this.end = end;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Workout.WorkoutCount.Request(paramsProvider, this.start, this.end).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Workout.WorkoutCount.Response))
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutCount.Response.class);
Workout.WorkoutCount.Response packet = (Workout.WorkoutCount.Response) receivedPacket;
if (packet.count == 0 || packet.workoutNumbers == null || packet.error != null) {
if(packet.error != null) {
LOG.warn("Error occurred during workout sync: {}", packet.error);
GB.toast("Error occurred during workout sync", Toast.LENGTH_LONG, GB.WARN);
}
this.supportProvider.endOfWorkoutSync();
return;
}
if (packet.count > packet.workoutNumbers.size()) {
LOG.warn("Packet count is greater than workoutNumbers size: {} > {}", packet.count, packet.workoutNumbers.size());
GB.toast("Workout count mismatch, after this sync is complete, try synchronising again", Toast.LENGTH_LONG, GB.WARN);
packet.count = (short) packet.workoutNumbers.size();
} else if (packet.count < packet.workoutNumbers.size()) {
LOG.warn("Packet count is smaller than workoutNumbers size: {} < {}", packet.count, packet.workoutNumbers.size());
GB.toast("Workout count mismatch, after this sync is complete, try synchronising again", Toast.LENGTH_LONG, GB.WARN);
packet.workoutNumbers.subList(packet.count, packet.workoutNumbers.size()).clear();
}
// Has to be sorted for the timestamp-based sync start that we use in the HuaweiSupportProvider
packet.workoutNumbers.sort(Comparator.comparingInt(o -> o.workoutNumber));
if (packet.count > 0) {
GetWorkoutTotalsRequest nextRequest = new GetWorkoutTotalsRequest(
this.supportProvider,
packet.workoutNumbers.remove(0),
packet.workoutNumbers
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
}
}
}
@@ -1,171 +0,0 @@
/* 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
public class GetWorkoutDataRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutDataRequest.class);
Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers;
List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder;
short number;
Long databaseId;
/**
* Request to get workout totals
*
* @param support The support
* @param workoutNumbers The numbers of the current workout
* @param remainder The numbers of the remainder if the workouts to get
* @param number The number of this data request
*/
public GetWorkoutDataRequest(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder, short number, Long databaseId) {
super(support);
this.serviceId = Workout.id;
this.commandId = Workout.WorkoutData.id;
this.workoutNumbers = workoutNumbers;
this.remainder = remainder;
this.number = number;
this.databaseId = databaseId;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Workout.WorkoutData.Request(paramsProvider, workoutNumbers.workoutNumber, this.number, supportProvider.getDeviceState().isSupportsWorkoutNewSteps()).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Workout.WorkoutData.Response))
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutData.Response.class);
Workout.WorkoutData.Response packet = (Workout.WorkoutData.Response) receivedPacket;
if (packet.workoutNumber != this.workoutNumbers.workoutNumber)
throw new WorkoutParseException("Incorrect workout number!");
if (packet.dataNumber != this.number)
throw new WorkoutParseException("Incorrect data number!");
LOG.info("Workout {} data {}:", this.workoutNumbers.workoutNumber, this.number);
LOG.info("Workout : {}", packet.workoutNumber);
LOG.info("Data num: {}", packet.dataNumber);
LOG.info("Header : {}", Arrays.toString(packet.rawHeader));
LOG.info("Header : {}", packet.header);
LOG.info("Data : {}", Arrays.toString(packet.rawData));
LOG.info("Data : {}", Arrays.toString(packet.dataList.toArray()));
LOG.info("Bitmap : {}", packet.innerBitmap);
this.supportProvider.addWorkoutSampleData(
this.databaseId,
packet.dataList
);
if (this.workoutNumbers.dataCount > this.number + 1) {
GetWorkoutDataRequest nextRequest = new GetWorkoutDataRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) (this.number + 1),
databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.paceCount > 0) {
GetWorkoutPaceRequest nextRequest = new GetWorkoutPaceRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.segmentsCount > 0) {
GetWorkoutSwimSegmentsRequest nextRequest = new GetWorkoutSwimSegmentsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.spO2Count > 0) {
GetWorkoutSpO2Request nextRequest = new GetWorkoutSpO2Request(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.sectionsCount > 0) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() {
@Override
public void run() {
if (!remainder.isEmpty()) {
GetWorkoutTotalsRequest nextRequest = new GetWorkoutTotalsRequest(
GetWorkoutDataRequest.this.supportProvider,
remainder.remove(0),
remainder
);
nextRequest.setFinalizeReq(GetWorkoutDataRequest.this.finalizeReq);
// Cannot do this with nextRequest because it's in a callback
try {
nextRequest.doPerform();
} catch (IOException e) {
finalizeReq.handleException(new ResponseParseException("Cannot send next request", e));
}
} else {
supportProvider.endOfWorkoutSync();
}
}
});
}
}
}
@@ -1,136 +0,0 @@
/* 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class GetWorkoutPaceRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutPaceRequest.class);
Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers;
List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder;
short number;
Long databaseId;
public GetWorkoutPaceRequest(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder, short number, Long databaseId) {
super(support);
this.serviceId = Workout.id;
this.commandId = Workout.WorkoutPace.id;
this.workoutNumbers = workoutNumbers;
this.remainder = remainder;
this.number = number;
this.databaseId = databaseId;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Workout.WorkoutPace.Request(paramsProvider, this.workoutNumbers.workoutNumber, this.number).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Workout.WorkoutPace.Response))
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutPace.Response.class);
Workout.WorkoutPace.Response packet = (Workout.WorkoutPace.Response) receivedPacket;
if (packet.error != null) {
LOG.warn("Error {} occurred during workout pace sync. ignoring", packet.error);
GB.toast("Error occurred during workout sync", Toast.LENGTH_LONG, GB.WARN);
supportProvider.nextWorkoutSync(remainder, GetWorkoutPaceRequest.this.finalizeReq);
return;
}
if (packet.workoutNumber != this.workoutNumbers.workoutNumber)
throw new WorkoutParseException("Incorrect workout number!");
if (packet.paceNumber != this.number)
throw new WorkoutParseException("Incorrect pace number!");
LOG.info("Workout {} pace {}:", this.workoutNumbers.workoutNumber, this.number);
LOG.info("Workout : {}", packet.workoutNumber);
LOG.info("Pace : {}", packet.paceNumber);
LOG.info("Block num: {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
supportProvider.addWorkoutPaceData(this.databaseId, packet.blocks, packet.paceNumber);
if (this.workoutNumbers.paceCount > this.number + 1) {
GetWorkoutPaceRequest nextRequest = new GetWorkoutPaceRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) (this.number + 1),
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.segmentsCount > 0) {
GetWorkoutSwimSegmentsRequest nextRequest = new GetWorkoutSwimSegmentsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.spO2Count > 0) {
GetWorkoutSpO2Request nextRequest = new GetWorkoutSpO2Request(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.sectionsCount > 0) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, () -> supportProvider.nextWorkoutSync(remainder, GetWorkoutPaceRequest.this.finalizeReq));
}
}
}
@@ -1,103 +0,0 @@
/* Copyright (C) 2024-2025 Damien Gaignon, Martin.JM, Me7c7
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.huawei.requests;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class GetWorkoutSectionsRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutSectionsRequest.class);
Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers;
List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder;
short number;
Long databaseId;
public GetWorkoutSectionsRequest(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder, short number, Long databaseId) {
super(support);
this.serviceId = Workout.id;
this.commandId = Workout.WorkoutSections.id;
this.workoutNumbers = workoutNumbers;
this.remainder = remainder;
this.number = number;
this.databaseId = databaseId;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Workout.WorkoutSections.Request(paramsProvider, this.workoutNumbers.workoutNumber, this.number).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Workout.WorkoutSections.Response))
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutSections.Response.class);
Workout.WorkoutSections.Response packet = (Workout.WorkoutSections.Response) receivedPacket;
if (packet.error == null) {
LOG.info("Workout {} section {}:", this.workoutNumbers.workoutNumber, this.number);
LOG.info("workoutId : {}", packet.workoutId);
LOG.info("number : {}", packet.number);
LOG.info("Block num : {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
supportProvider.addWorkoutSectionsData(this.databaseId, packet.blocks, this.number);
} else if (packet.error == 0x0001E079) {
// We don't know why it often returns this error, but it seems to be normal, so we ignore it
LOG.warn("Error 0001E079 occurred during workout swim segments sync. This seems to be normal, so it's ignored.");
} else {
LOG.warn("Error {} occurred during workout swim segments sync. Skipping workout", packet.error);
GB.toast("Error occurred during workout sync", Toast.LENGTH_LONG, GB.WARN);
supportProvider.nextWorkoutSync(remainder, GetWorkoutSectionsRequest.this.finalizeReq);
return;
}
if (this.workoutNumbers.sectionsCount > this.number + 1) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) (this.number + 1),
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, () -> supportProvider.nextWorkoutSync(remainder, GetWorkoutSectionsRequest.this.finalizeReq));
}
}
}
@@ -1,111 +0,0 @@
/* Copyright (C) 2024-2025 Damien Gaignon, Martin.JM, Me7c7
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.huawei.requests;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class GetWorkoutSpO2Request extends Request {
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutSpO2Request.class);
Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers;
List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder;
short number;
Long databaseId;
public GetWorkoutSpO2Request(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder, short number, Long databaseId) {
super(support);
this.serviceId = Workout.id;
this.commandId = Workout.WorkoutSpO2.id;
this.workoutNumbers = workoutNumbers;
this.remainder = remainder;
this.number = number;
this.databaseId = databaseId;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Workout.WorkoutSpO2.Request(paramsProvider, this.workoutNumbers.workoutNumber, this.number).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Workout.WorkoutSpO2.Response))
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutSections.Response.class);
Workout.WorkoutSpO2.Response packet = (Workout.WorkoutSpO2.Response) receivedPacket;
if (packet.error != null) {
LOG.warn("Error {} occurred during workout swim segments sync. ignoring", packet.error);
GB.toast("Error occurred during workout sync", Toast.LENGTH_LONG, GB.WARN);
supportProvider.nextWorkoutSync(remainder, GetWorkoutSpO2Request.this.finalizeReq);
return;
}
LOG.info("Workout {} current {}:", this.workoutNumbers.workoutNumber, this.number);
LOG.info("spO2Number1: {}", packet.spO2Number1);
LOG.info("spO2Number2: {}", packet.spO2Number2);
LOG.info("Block num : {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
supportProvider.addWorkoutSpO2Data(this.databaseId, packet.blocks, this.number);
if (this.workoutNumbers.spO2Count > this.number + 1) {
GetWorkoutSpO2Request nextRequest = new GetWorkoutSpO2Request(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) (this.number + 1),
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.sectionsCount > 0) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, () -> supportProvider.nextWorkoutSync(remainder, GetWorkoutSpO2Request.this.finalizeReq));
}
}
}
@@ -1,127 +0,0 @@
/* Copyright (C) 2024-2025 Damien Gaignon, Martin.JM, Me7c7
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.huawei.requests;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class GetWorkoutSwimSegmentsRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutSwimSegmentsRequest.class);
Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers;
List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder;
short number;
Long databaseId;
public GetWorkoutSwimSegmentsRequest(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder, short number, Long databaseId) {
super(support);
this.serviceId = Workout.id;
this.commandId = Workout.WorkoutSwimSegments.id;
this.workoutNumbers = workoutNumbers;
this.remainder = remainder;
this.number = number;
this.databaseId = databaseId;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Workout.WorkoutSwimSegments.Request(paramsProvider, this.workoutNumbers.workoutNumber, this.number).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Workout.WorkoutSwimSegments.Response))
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutSwimSegments.Response.class);
Workout.WorkoutSwimSegments.Response packet = (Workout.WorkoutSwimSegments.Response) receivedPacket;
if (packet.error != null) {
LOG.warn("Error {} occurred during workout swim segments sync. ignoring", packet.error);
GB.toast("Error occurred during workout sync", Toast.LENGTH_LONG, GB.WARN);
supportProvider.nextWorkoutSync(remainder, GetWorkoutSwimSegmentsRequest.this.finalizeReq);
return;
}
if (packet.workoutNumber != this.workoutNumbers.workoutNumber)
throw new WorkoutParseException("Incorrect workout number!");
if (packet.segmentNumber != this.number)
throw new WorkoutParseException("Incorrect pace number!");
LOG.info("Workout {} segment {}:", this.workoutNumbers.workoutNumber, this.number);
LOG.info("Workout : {}", packet.workoutNumber);
LOG.info("Segments : {}", packet.segmentNumber);
LOG.info("Block num: {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
supportProvider.addWorkoutSwimSegmentsData(this.databaseId, packet.blocks, packet.segmentNumber);
if (this.workoutNumbers.segmentsCount > this.number + 1) {
GetWorkoutSwimSegmentsRequest nextRequest = new GetWorkoutSwimSegmentsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) (this.number + 1),
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.spO2Count > 0) {
GetWorkoutSpO2Request nextRequest = new GetWorkoutSpO2Request(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.sectionsCount > 0) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, () -> supportProvider.nextWorkoutSync(remainder, GetWorkoutSwimSegmentsRequest.this.finalizeReq));
}
}
}
@@ -1,152 +0,0 @@
/* 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class GetWorkoutTotalsRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutTotalsRequest.class);
Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers;
List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder;
/**
* Request to get workout totals
*
* @param support The support
* @param workoutNumbers The numbers of the current workout
* @param remainder The numbers of the remainder of the workouts to get
*/
public GetWorkoutTotalsRequest(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder) {
super(support);
this.serviceId = Workout.id;
this.commandId = Workout.WorkoutTotals.id;
this.workoutNumbers = workoutNumbers;
this.remainder = remainder;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Workout.WorkoutTotals.Request(paramsProvider, workoutNumbers.workoutNumber).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Workout.WorkoutTotals.Response))
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutTotals.Response.class);
Workout.WorkoutTotals.Response packet = (Workout.WorkoutTotals.Response) receivedPacket;
if (packet.error != null) {
LOG.warn("Error {} occurred during workout pace sync. ignoring", packet.error);
GB.toast("Error occurred during workout sync", Toast.LENGTH_LONG, GB.WARN);
supportProvider.nextWorkoutSync(remainder, GetWorkoutTotalsRequest.this.finalizeReq);
return;
}
if (packet.number != this.workoutNumbers.workoutNumber)
throw new WorkoutParseException("Incorrect workout number!");
LOG.info("Workout {} totals:", this.workoutNumbers.workoutNumber);
LOG.info("Number : {}", packet.number);
LOG.info("Status : {}", packet.status);
LOG.info("Start : {}", packet.startTime);
LOG.info("End : {}", packet.endTime);
LOG.info("Calories: {}", packet.calories);
LOG.info("Distance: {}", packet.distance);
LOG.info("Steps : {}", packet.stepCount);
LOG.info("Time : {}", packet.totalTime);
LOG.info("Duration: {}", packet.duration);
LOG.info("Type : {}", packet.type);
Long databaseId = this.supportProvider.addWorkoutTotalsData(packet);
// Create the next request
if (this.workoutNumbers.dataCount > 0) {
GetWorkoutDataRequest nextRequest = new GetWorkoutDataRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.paceCount > 0) {
GetWorkoutPaceRequest nextRequest = new GetWorkoutPaceRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.segmentsCount > 0) {
GetWorkoutSwimSegmentsRequest nextRequest = new GetWorkoutSwimSegmentsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.spO2Count > 0) {
GetWorkoutSpO2Request nextRequest = new GetWorkoutSpO2Request(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.sectionsCount > 0) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, databaseId, () -> supportProvider.nextWorkoutSync(remainder, GetWorkoutTotalsRequest.this.finalizeReq));
}
}
}
@@ -34,6 +34,8 @@ public class RequestBuilder<T extends HuaweiPacket> {
private boolean addToResponse = true;
private Integer timeout = null;
private OnCallback<T> onCallback = null;
private OnTimeout<T> onTimeout = null;
private OnException onException = null;
@@ -55,6 +57,11 @@ public class RequestBuilder<T extends HuaweiPacket> {
return this;
}
public RequestBuilder<T> setTimeout(Integer timeout) {
this.timeout = timeout;
return this;
}
public RequestBuilder<T> onCallback(OnCallback<T> onCallback) {
this.onCallback = onCallback;
return this;
@@ -76,6 +83,7 @@ public class RequestBuilder<T extends HuaweiPacket> {
r.commandId = commandId;
r.sendingPacket = requestPacket;
r.addToResponse = this.addToResponse;
r.setupTimeoutUntilNext(this.timeout);
r.setFinalizeReq(new Request.RequestCallback() {
@Override
public void call(Request request) {