Huawei: Re-parse workout details when opening details

This commit is contained in:
José Rebelo 2024-09-08 21:27:36 +01:00 committed by José Rebelo
parent 967ae31b7a
commit 183bf8725f
10 changed files with 148 additions and 111 deletions

View File

@ -23,19 +23,16 @@ import android.net.Uri;
import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
@ -49,9 +46,11 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiBRSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordinator implements HuaweiCoordinatorSupplier {
@ -209,6 +208,11 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
return huaweiCoordinator.getInstallHandler(uri, context);
}
@Override
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device, final Context context) {
return new HuaweiWorkoutGbParser(device, context);
}
@Override
public SampleProvider<? extends AbstractActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new HuaweiSampleProvider(device, session);

View File

@ -47,9 +47,11 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiLESupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator implements HuaweiCoordinatorSupplier {
@ -215,6 +217,11 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
return huaweiCoordinator.getInstallHandler(uri, context);
}
@Override
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device, final Context context) {
return new HuaweiWorkoutGbParser(device, context);
}
@Override
public SampleProvider<? extends AbstractActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new HuaweiSampleProvider(device, session);

View File

@ -70,7 +70,7 @@ public class HuaweiSettingsCustomizer implements DeviceSpecificSettingsCustomize
if (preference.getKey().equals("huawei_reparse_workout_data")) {
if (((SwitchPreferenceCompat) preference).isChecked()) {
GB.toast("Starting workout reparse", Toast.LENGTH_SHORT, 0);
HuaweiWorkoutGbParser.parseAllWorkouts();
new HuaweiWorkoutGbParser(handler.getDevice(), handler.getContext()).parseAllWorkouts();
GB.toast("Workout reparse is complete", Toast.LENGTH_SHORT, 0);
((SwitchPreferenceCompat) preference).setChecked(false);

View File

@ -90,12 +90,10 @@ public class ActivitySummaryJsonSummary {
}
private String getCorrectSummary(BaseActivitySummary item, final boolean forDetails){
if (item.getRawSummaryData() != null || item.getRawDetailsPath() != null) {
try {
item = summaryParser.parseBinaryData(item, forDetails);
} catch (final Exception e) {
LOG.error("Failed to re-parse corrected summary", e);
}
try {
item = summaryParser.parseBinaryData(item, forDetails);
} catch (final Exception e) {
LOG.error("Failed to re-parse corrected summary", e);
}
return item.getSummaryData();
}

View File

@ -16,7 +16,17 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
import java.util.Date;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public interface ActivitySummaryParser {
/**
@ -30,4 +40,31 @@ public interface ActivitySummaryParser {
* @return the update {@link BaseActivitySummary}
*/
BaseActivitySummary parseBinaryData(BaseActivitySummary summary, final boolean forDetails);
static BaseActivitySummary findOrCreateBaseActivitySummary(final DaoSession session,
final GBDevice gbDevice,
final int timestampSeconds) {
final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session);
final BaseActivitySummaryDao summaryDao = session.getBaseActivitySummaryDao();
final QueryBuilder<BaseActivitySummary> qb = summaryDao.queryBuilder();
qb.where(BaseActivitySummaryDao.Properties.StartTime.eq(new Date(timestampSeconds * 1000L)));
qb.where(BaseActivitySummaryDao.Properties.DeviceId.eq(device.getId()));
qb.where(BaseActivitySummaryDao.Properties.UserId.eq(user.getId()));
final List<BaseActivitySummary> summaries = qb.build().list();
if (summaries.isEmpty()) {
final BaseActivitySummary summary = new BaseActivitySummary();
summary.setStartTime(new Date(timestampSeconds * 1000L));
summary.setDevice(device);
summary.setUser(user);
// These will be set later, once we parse the summary
summary.setEndTime(new Date(timestampSeconds * 1000L));
summary.setActivityKind(ActivityKind.UNKNOWN.getCode());
return summary;
}
return summaries.get(0);
}
}

View File

@ -46,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
@ -299,9 +300,11 @@ public class FitImporter {
// This ensures idempotency when re-processing
try (DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession();
final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session);
summary = findOrCreateBaseActivitySummary(session, device, user, Objects.requireNonNull(fileId.getTimeCreated()).intValue());
summary = ActivitySummaryParser.findOrCreateBaseActivitySummary(
session,
gbDevice,
Objects.requireNonNull(fileId.getTimeCreated()).intValue()
);
} catch (final Exception e) {
GB.toast(context, "Error finding base summary", Toast.LENGTH_LONG, GB.ERROR, e);
return;
@ -325,34 +328,6 @@ public class FitImporter {
}
}
protected static BaseActivitySummary findOrCreateBaseActivitySummary(final DaoSession session,
final Device device,
final User user,
final int timestampSeconds) {
final BaseActivitySummaryDao summaryDao = session.getBaseActivitySummaryDao();
final QueryBuilder<BaseActivitySummary> qb = summaryDao.queryBuilder();
qb.where(BaseActivitySummaryDao.Properties.StartTime.eq(new Date(timestampSeconds * 1000L)));
qb.where(BaseActivitySummaryDao.Properties.DeviceId.eq(device.getId()));
qb.where(BaseActivitySummaryDao.Properties.UserId.eq(user.getId()));
final List<BaseActivitySummary> summaries = qb.build().list();
if (summaries.isEmpty()) {
final BaseActivitySummary summary = new BaseActivitySummary();
summary.setStartTime(new Date(timestampSeconds * 1000L));
summary.setDevice(device);
summary.setUser(user);
// These will be set later, once we parse the summary
summary.setEndTime(new Date(timestampSeconds * 1000L));
summary.setActivityKind(ActivityKind.UNKNOWN.getCode());
return summary;
}
if (summaries.size() > 1) {
LOG.warn("Found multiple summaries for {}", timestampSeconds);
}
return summaries.get(0);
}
private void reset() {
activitySamplesPerTimestamp.clear();
stressSamples.clear();

View File

@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
import android.content.Context;
import android.widget.Toast;
import org.slf4j.Logger;
@ -32,18 +33,23 @@ import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
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.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.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.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
@ -52,11 +58,49 @@ import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
* 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 {
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<HuaweiWorkoutSummarySample> 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<HuaweiWorkoutSummarySample> huaweiSummaries = qb.build().list();
if (huaweiSummaries.isEmpty()) {
LOG.warn("Failed to find huawei summary for {}", summary.getStartTime());
return summary;
}
updateBaseSummary(db, 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
@ -159,7 +203,7 @@ public class HuaweiWorkoutGbParser {
MOTOR_AUTO_RACING(216, ActivityKind.AUTO_RACING),
ESPORTS(223, ActivityKind.ESPORTS),
PADEL(224, ActivityKind.PADEL),
OTHER(255, ActivityKind.EXERCISE)
OTHER(255, ActivityKind.EXERCISE),
;
private final byte type;
@ -187,7 +231,7 @@ public class HuaweiWorkoutGbParser {
}
}
public static void parseAllWorkouts() {
public void parseAllWorkouts() {
parseUnknownWorkoutData();
try (DBHandler db = GBApplication.acquireDB()) {
@ -261,7 +305,7 @@ public class HuaweiWorkoutGbParser {
return ActivityKind.UNKNOWN;
}
public static void parseWorkout(Long workoutId) {
public void parseWorkout(Long workoutId) {
if (workoutId == null)
return;
@ -274,32 +318,34 @@ public class HuaweiWorkoutGbParser {
return;
HuaweiWorkoutSummarySample summary = summarySamples.get(0);
final BaseActivitySummary baseSummary = ActivitySummaryParser.findOrCreateBaseActivitySummary(
db.getDaoSession(),
gbDevice,
summary.getStartTimestamp()
);
updateBaseSummary(db, summary, baseSummary);
db.getDaoSession().getBaseActivitySummaryDao().insertOrReplace(baseSummary);
} catch (Exception e) {
GB.toast("Exception parsing workout data", Toast.LENGTH_SHORT, GB.ERROR, e);
LOG.error("Exception parsing workout data", e);
}
}
public void updateBaseSummary(final DBHandler db,
final HuaweiWorkoutSummarySample summary,
final BaseActivitySummary baseSummary) {
try {
QueryBuilder<HuaweiWorkoutDataSample> qbData = db.getDaoSession().getHuaweiWorkoutDataSampleDao().queryBuilder().where(
HuaweiWorkoutDataSampleDao.Properties.WorkoutId.eq(workoutId)
HuaweiWorkoutDataSampleDao.Properties.WorkoutId.eq(summary.getWorkoutId())
);
List<HuaweiWorkoutDataSample> dataSamples = qbData.build().list();
QueryBuilder<HuaweiWorkoutPaceSample> qbPace = db.getDaoSession().getHuaweiWorkoutPaceSampleDao().queryBuilder().where(
HuaweiWorkoutPaceSampleDao.Properties.WorkoutId.eq(workoutId)
HuaweiWorkoutPaceSampleDao.Properties.WorkoutId.eq(summary.getWorkoutId())
);
long userId = summary.getUserId();
long deviceId = summary.getDeviceId();
Date start = new Date(summary.getStartTimestamp() * 1000L);
Date end = new Date(summary.getEndTimestamp() * 1000L);
// Avoid duplicates
QueryBuilder<BaseActivitySummary> qb = db.getDaoSession().getBaseActivitySummaryDao().queryBuilder().where(
BaseActivitySummaryDao.Properties.UserId.eq(userId),
BaseActivitySummaryDao.Properties.DeviceId.eq(deviceId),
BaseActivitySummaryDao.Properties.StartTime.eq(start),
BaseActivitySummaryDao.Properties.EndTime.eq(end)
);
List<BaseActivitySummary> duplicates = qb.build().list();
BaseActivitySummary previous = null;
if (!duplicates.isEmpty())
previous = duplicates.get(0);
ActivityKind type = huaweiTypeToGbType(summary.getType());
ActivitySummaryData summaryData = new ActivitySummaryData();
@ -665,43 +711,13 @@ public class HuaweiWorkoutGbParser {
);
}
BaseActivitySummary baseSummary;
if (previous == null) {
baseSummary = new BaseActivitySummary(
null,
"Workout " + summary.getWorkoutNumber(),
start,
end,
type.getCode(),
null,
null,
null,
null,
null,
deviceId,
userId,
summaryData.toString(),
null
);
} else {
baseSummary = new BaseActivitySummary(
previous.getId(),
previous.getName(),
start,
end,
type.getCode(),
previous.getBaseLongitude(),
previous.getBaseLatitude(),
previous.getBaseAltitude(),
previous.getGpxTrack(),
previous.getRawDetailsPath(),
deviceId,
userId,
summaryData.toString(),
null
);
if (baseSummary.getName() == null) {
baseSummary.setName("Workout " + summary.getWorkoutNumber());
}
db.getDaoSession().getBaseActivitySummaryDao().insertOrReplace(baseSummary);
// 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);

View File

@ -112,9 +112,9 @@ public class GetWorkoutDataRequest extends Request {
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
HuaweiWorkoutGbParser.parseWorkout(this.databaseId);
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
if (remainder.size() > 0) {
if (!remainder.isEmpty()) {
GetWorkoutTotalsRequest nextRequest = new GetWorkoutTotalsRequest(
this.supportProvider,
remainder.remove(0),

View File

@ -89,9 +89,9 @@ public class GetWorkoutPaceRequest extends Request {
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
HuaweiWorkoutGbParser.parseWorkout(this.databaseId);
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
if (remainder.size() > 0) {
if (!remainder.isEmpty()) {
GetWorkoutTotalsRequest nextRequest = new GetWorkoutTotalsRequest(
this.supportProvider,
remainder.remove(0),

View File

@ -103,9 +103,9 @@ public class GetWorkoutTotalsRequest extends Request {
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
HuaweiWorkoutGbParser.parseWorkout(databaseId);
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(databaseId);
if (remainder.size() > 0) {
if (!remainder.isEmpty()) {
GetWorkoutTotalsRequest nextRequest = new GetWorkoutTotalsRequest(
this.supportProvider,
remainder.remove(0),