feat(core): consolide le chainage type metier d'exercice -> strategie Health Services -> payload montre -> consommation Kotlin
Finalise #183 : modele/persistance des types metier d'exercice, mapping vers la strategie Health Services, propagation du payload dans le contrat watch bridge et consommation cote Kotlin montre. Perimetre complet QA vert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -3542,7 +3542,11 @@ bool _hasSameWatchCommandRevisionState(
|
|||||||
left.manualScoreTargetValue == right.manualScoreTargetValue &&
|
left.manualScoreTargetValue == right.manualScoreTargetValue &&
|
||||||
left.manualScoreTargetLabel == right.manualScoreTargetLabel &&
|
left.manualScoreTargetLabel == right.manualScoreTargetLabel &&
|
||||||
left.manualScoreRepsTargetValue == right.manualScoreRepsTargetValue &&
|
left.manualScoreRepsTargetValue == right.manualScoreRepsTargetValue &&
|
||||||
left.manualScoreScope == right.manualScoreScope;
|
left.manualScoreScope == right.manualScoreScope &&
|
||||||
|
_listEquals(
|
||||||
|
left.healthServicesExerciseTypeStrategy,
|
||||||
|
right.healthServicesExerciseTypeStrategy,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _hasSameWatchCommandRevisionTimerListState(
|
bool _hasSameWatchCommandRevisionTimerListState(
|
||||||
@ -4216,6 +4220,8 @@ final class WatchSessionProjectionProjector {
|
|||||||
manualScoreTargetLabel: manualScoreProjection?.targetLabel,
|
manualScoreTargetLabel: manualScoreProjection?.targetLabel,
|
||||||
manualScoreRepsTargetValue: manualScoreProjection?.repsTargetValue,
|
manualScoreRepsTargetValue: manualScoreProjection?.repsTargetValue,
|
||||||
manualScoreScope: manualScoreProjection?.scope,
|
manualScoreScope: manualScoreProjection?.scope,
|
||||||
|
healthServicesExerciseTypeStrategy:
|
||||||
|
snapshot.healthServicesExerciseTypeStrategy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7052,6 +7058,13 @@ ScoreInputMode _scoreInputModeFromSnapshot(Object? value) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> _stringListFromSnapshot(Object? value) {
|
||||||
|
if (value is! List) {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
return value.whereType<String>().toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
_SetPositionSnapshot? _findSetSnapshot({
|
_SetPositionSnapshot? _findSetSnapshot({
|
||||||
required String resolvedTemplateSnapshotJson,
|
required String resolvedTemplateSnapshotJson,
|
||||||
required int programIndex,
|
required int programIndex,
|
||||||
@ -7203,6 +7216,9 @@ _ResolvedExerciseSnapshot? _findExerciseSnapshot({
|
|||||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||||
setsCount: exercise['setsCount'] as int? ?? 0,
|
setsCount: exercise['setsCount'] as int? ?? 0,
|
||||||
restSeconds: exercise['restSecondsOverride'] as int? ?? 0,
|
restSeconds: exercise['restSecondsOverride'] as int? ?? 0,
|
||||||
|
healthServicesExerciseTypeStrategy: _stringListFromSnapshot(
|
||||||
|
exercise['healthServicesExerciseTypeStrategy'],
|
||||||
|
),
|
||||||
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
||||||
autoStartNextTimedStepEffective: autoStartNextTimedStepEffective,
|
autoStartNextTimedStepEffective: autoStartNextTimedStepEffective,
|
||||||
);
|
);
|
||||||
@ -7423,6 +7439,9 @@ Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
|
|||||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||||
setsCount: exercise['setsCount'] as int? ?? 0,
|
setsCount: exercise['setsCount'] as int? ?? 0,
|
||||||
restSeconds: exercise['restSecondsOverride'] as int? ?? 0,
|
restSeconds: exercise['restSecondsOverride'] as int? ?? 0,
|
||||||
|
healthServicesExerciseTypeStrategy: _stringListFromSnapshot(
|
||||||
|
exercise['healthServicesExerciseTypeStrategy'],
|
||||||
|
),
|
||||||
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
||||||
autoStartNextTimedStepEffective:
|
autoStartNextTimedStepEffective:
|
||||||
(exercise['autoStartNextTimedStepOverride'] as bool?) ??
|
(exercise['autoStartNextTimedStepOverride'] as bool?) ??
|
||||||
@ -7453,6 +7472,7 @@ final class _ResolvedExerciseSnapshot {
|
|||||||
this.scoreUnitSnapshot,
|
this.scoreUnitSnapshot,
|
||||||
required this.setsCount,
|
required this.setsCount,
|
||||||
required this.restSeconds,
|
required this.restSeconds,
|
||||||
|
this.healthServicesExerciseTypeStrategy = const [],
|
||||||
this.steps = const [],
|
this.steps = const [],
|
||||||
this.autoStartNextTimedStepEffective = true,
|
this.autoStartNextTimedStepEffective = true,
|
||||||
});
|
});
|
||||||
@ -7474,6 +7494,7 @@ final class _ResolvedExerciseSnapshot {
|
|||||||
final String? scoreUnitSnapshot;
|
final String? scoreUnitSnapshot;
|
||||||
final int setsCount;
|
final int setsCount;
|
||||||
final int restSeconds;
|
final int restSeconds;
|
||||||
|
final List<String> healthServicesExerciseTypeStrategy;
|
||||||
final List<ExerciseStep> steps;
|
final List<ExerciseStep> steps;
|
||||||
final bool autoStartNextTimedStepEffective;
|
final bool autoStartNextTimedStepEffective;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,17 @@ enum BusinessExerciseType {
|
|||||||
recovery,
|
recovery,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HealthServicesExerciseType {
|
||||||
|
running('RUNNING'),
|
||||||
|
walking('WALKING'),
|
||||||
|
highIntensityIntervalTraining('HIGH_INTENSITY_INTERVAL_TRAINING'),
|
||||||
|
workout('WORKOUT');
|
||||||
|
|
||||||
|
const HealthServicesExerciseType(this.wireName);
|
||||||
|
|
||||||
|
final String wireName;
|
||||||
|
}
|
||||||
|
|
||||||
enum ActiveWorkoutStatus { running, paused, savedExit, completed, abandoned }
|
enum ActiveWorkoutStatus { running, paused, savedExit, completed, abandoned }
|
||||||
|
|
||||||
enum SetResultStatus { completed, skipped }
|
enum SetResultStatus { completed, skipped }
|
||||||
@ -439,6 +450,11 @@ final class Exercise {
|
|||||||
? _businessTypesFromCategory(category)
|
? _businessTypesFromCategory(category)
|
||||||
: businessTypes;
|
: businessTypes;
|
||||||
|
|
||||||
|
List<HealthServicesExerciseType> get healthServicesExerciseTypeStrategy =>
|
||||||
|
healthServicesExerciseTypeStrategyForBusinessTypes(
|
||||||
|
effectiveBusinessTypes,
|
||||||
|
);
|
||||||
|
|
||||||
Exercise archive(DateTime now) {
|
Exercise archive(DateTime now) {
|
||||||
return copyWith(archivedAt: now, metadata: metadata.touch(now));
|
return copyWith(archivedAt: now, metadata: metadata.touch(now));
|
||||||
}
|
}
|
||||||
@ -639,6 +655,9 @@ final class ProgramExercise {
|
|||||||
this.exerciseImageMediaIdSnapshot,
|
this.exerciseImageMediaIdSnapshot,
|
||||||
List<String> exerciseImageMediaIdsSnapshot = const [],
|
List<String> exerciseImageMediaIdsSnapshot = const [],
|
||||||
this.exerciseVideoMediaIdSnapshot,
|
this.exerciseVideoMediaIdSnapshot,
|
||||||
|
List<HealthServicesExerciseType>
|
||||||
|
healthServicesExerciseTypeStrategySnapshot =
|
||||||
|
const [],
|
||||||
List<ExerciseStep> exerciseStepsSnapshot = const [],
|
List<ExerciseStep> exerciseStepsSnapshot = const [],
|
||||||
this.autoStartNextTimedStepSnapshot = true,
|
this.autoStartNextTimedStepSnapshot = true,
|
||||||
this.autoStartNextTimedStepOverride,
|
this.autoStartNextTimedStepOverride,
|
||||||
@ -668,6 +687,12 @@ final class ProgramExercise {
|
|||||||
? [exerciseImageMediaIdSnapshot]
|
? [exerciseImageMediaIdSnapshot]
|
||||||
: exerciseImageMediaIdsSnapshot,
|
: exerciseImageMediaIdsSnapshot,
|
||||||
),
|
),
|
||||||
|
healthServicesExerciseTypeStrategySnapshot =
|
||||||
|
_validatedHealthServicesExerciseTypes(
|
||||||
|
healthServicesExerciseTypeStrategySnapshot.isEmpty
|
||||||
|
? legacyHealthServicesExerciseTypeStrategy
|
||||||
|
: healthServicesExerciseTypeStrategySnapshot,
|
||||||
|
),
|
||||||
exerciseStepsSnapshot = _validatedExerciseSteps(exerciseStepsSnapshot) {
|
exerciseStepsSnapshot = _validatedExerciseSteps(exerciseStepsSnapshot) {
|
||||||
_requireNonNegative(position, 'Position');
|
_requireNonNegative(position, 'Position');
|
||||||
_requirePositive(setsCount, 'Sets count');
|
_requirePositive(setsCount, 'Sets count');
|
||||||
@ -712,6 +737,8 @@ final class ProgramExercise {
|
|||||||
final String? exerciseImageMediaIdSnapshot;
|
final String? exerciseImageMediaIdSnapshot;
|
||||||
final List<String> exerciseImageMediaIdsSnapshot;
|
final List<String> exerciseImageMediaIdsSnapshot;
|
||||||
final String? exerciseVideoMediaIdSnapshot;
|
final String? exerciseVideoMediaIdSnapshot;
|
||||||
|
final List<HealthServicesExerciseType>
|
||||||
|
healthServicesExerciseTypeStrategySnapshot;
|
||||||
final List<ExerciseStep> exerciseStepsSnapshot;
|
final List<ExerciseStep> exerciseStepsSnapshot;
|
||||||
final bool autoStartNextTimedStepSnapshot;
|
final bool autoStartNextTimedStepSnapshot;
|
||||||
final bool? autoStartNextTimedStepOverride;
|
final bool? autoStartNextTimedStepOverride;
|
||||||
@ -761,6 +788,8 @@ final class ProgramExercise {
|
|||||||
exerciseImageMediaIdSnapshot: exercise.imageMediaId,
|
exerciseImageMediaIdSnapshot: exercise.imageMediaId,
|
||||||
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
|
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
|
||||||
exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
|
exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
|
||||||
|
healthServicesExerciseTypeStrategySnapshot:
|
||||||
|
exercise.healthServicesExerciseTypeStrategy,
|
||||||
exerciseStepsSnapshot: exercise.steps,
|
exerciseStepsSnapshot: exercise.steps,
|
||||||
autoStartNextTimedStepSnapshot: exercise.autoStartNextTimedStep,
|
autoStartNextTimedStepSnapshot: exercise.autoStartNextTimedStep,
|
||||||
exerciseArchivedSnapshot: exercise.archivedAt != null,
|
exerciseArchivedSnapshot: exercise.archivedAt != null,
|
||||||
@ -792,6 +821,10 @@ final class ProgramExercise {
|
|||||||
'exerciseImageMediaIdsSnapshot': exerciseImageMediaIdsSnapshot,
|
'exerciseImageMediaIdsSnapshot': exerciseImageMediaIdsSnapshot,
|
||||||
'imageMediaIdsSnapshot': exerciseImageMediaIdsSnapshot,
|
'imageMediaIdsSnapshot': exerciseImageMediaIdsSnapshot,
|
||||||
'exerciseVideoMediaIdSnapshot': exerciseVideoMediaIdSnapshot,
|
'exerciseVideoMediaIdSnapshot': exerciseVideoMediaIdSnapshot,
|
||||||
|
'healthServicesExerciseTypeStrategy':
|
||||||
|
healthServicesExerciseTypeStrategySnapshot
|
||||||
|
.map((type) => type.wireName)
|
||||||
|
.toList(),
|
||||||
'exerciseStepsSnapshot': exerciseStepsSnapshot
|
'exerciseStepsSnapshot': exerciseStepsSnapshot
|
||||||
.map((step) => step.toSnapshotJson())
|
.map((step) => step.toSnapshotJson())
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -2021,6 +2054,25 @@ List<BusinessExerciseType> _validatedBusinessExerciseTypes(
|
|||||||
return List.unmodifiable(unique);
|
return List.unmodifiable(unique);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<HealthServicesExerciseType> _validatedHealthServicesExerciseTypes(
|
||||||
|
List<HealthServicesExerciseType> types,
|
||||||
|
) {
|
||||||
|
final unique = <HealthServicesExerciseType>[];
|
||||||
|
for (final type in types) {
|
||||||
|
if (!unique.contains(type)) {
|
||||||
|
unique.add(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.unmodifiable(unique);
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyHealthServicesExerciseTypeStrategy = [
|
||||||
|
HealthServicesExerciseType.running,
|
||||||
|
HealthServicesExerciseType.walking,
|
||||||
|
HealthServicesExerciseType.highIntensityIntervalTraining,
|
||||||
|
HealthServicesExerciseType.workout,
|
||||||
|
];
|
||||||
|
|
||||||
List<BusinessExerciseType> _businessTypesFromCategory(
|
List<BusinessExerciseType> _businessTypesFromCategory(
|
||||||
ExerciseCategory category,
|
ExerciseCategory category,
|
||||||
) {
|
) {
|
||||||
@ -2036,6 +2088,49 @@ List<BusinessExerciseType> _businessTypesFromCategory(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<HealthServicesExerciseType>
|
||||||
|
healthServicesExerciseTypeStrategyForBusinessTypes(
|
||||||
|
List<BusinessExerciseType> types,
|
||||||
|
) {
|
||||||
|
if (types.isEmpty) {
|
||||||
|
return legacyHealthServicesExerciseTypeStrategy;
|
||||||
|
}
|
||||||
|
final output = <HealthServicesExerciseType>[];
|
||||||
|
void add(HealthServicesExerciseType type) {
|
||||||
|
if (!output.contains(type)) {
|
||||||
|
output.add(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final type in types) {
|
||||||
|
switch (type) {
|
||||||
|
case BusinessExerciseType.dribble:
|
||||||
|
case BusinessExerciseType.finishing:
|
||||||
|
add(HealthServicesExerciseType.running);
|
||||||
|
add(HealthServicesExerciseType.highIntensityIntervalTraining);
|
||||||
|
add(HealthServicesExerciseType.workout);
|
||||||
|
case BusinessExerciseType.conditioning:
|
||||||
|
case BusinessExerciseType.highIntensity:
|
||||||
|
case BusinessExerciseType.defense:
|
||||||
|
add(HealthServicesExerciseType.highIntensityIntervalTraining);
|
||||||
|
add(HealthServicesExerciseType.running);
|
||||||
|
add(HealthServicesExerciseType.workout);
|
||||||
|
case BusinessExerciseType.mobility:
|
||||||
|
case BusinessExerciseType.recovery:
|
||||||
|
add(HealthServicesExerciseType.walking);
|
||||||
|
add(HealthServicesExerciseType.running);
|
||||||
|
add(HealthServicesExerciseType.workout);
|
||||||
|
case BusinessExerciseType.shoot:
|
||||||
|
case BusinessExerciseType.freeThrows:
|
||||||
|
add(HealthServicesExerciseType.highIntensityIntervalTraining);
|
||||||
|
add(HealthServicesExerciseType.running);
|
||||||
|
add(HealthServicesExerciseType.walking);
|
||||||
|
add(HealthServicesExerciseType.workout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.unmodifiable(output);
|
||||||
|
}
|
||||||
|
|
||||||
void _requireAtLeastOneMeasure({
|
void _requireAtLeastOneMeasure({
|
||||||
required bool hasTime,
|
required bool hasTime,
|
||||||
required bool hasReps,
|
required bool hasReps,
|
||||||
|
|||||||
@ -968,6 +968,16 @@ FROM pending_share_actions
|
|||||||
"business_types_json TEXT NOT NULL DEFAULT '[]' "
|
"business_types_json TEXT NOT NULL DEFAULT '[]' "
|
||||||
'CHECK (json_valid(business_types_json))',
|
'CHECK (json_valid(business_types_json))',
|
||||||
);
|
);
|
||||||
|
await _addColumnIfMissing(
|
||||||
|
tableName: 'program_exercises',
|
||||||
|
columnName: 'health_services_exercise_type_strategy_snapshot_json',
|
||||||
|
definition:
|
||||||
|
'health_services_exercise_type_strategy_snapshot_json TEXT NOT NULL '
|
||||||
|
"DEFAULT '[\"RUNNING\",\"WALKING\","
|
||||||
|
"\"HIGH_INTENSITY_INTERVAL_TRAINING\",\"WORKOUT\"]' "
|
||||||
|
'CHECK (json_valid('
|
||||||
|
'health_services_exercise_type_strategy_snapshot_json))',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _backfillWorkoutHistorySetSourceExerciseIds() async {
|
Future<void> _backfillWorkoutHistorySetSourceExerciseIds() async {
|
||||||
|
|||||||
@ -193,6 +193,10 @@ final class DriftStarterSeedRepository
|
|||||||
await database
|
await database
|
||||||
.into(database.programExercises)
|
.into(database.programExercises)
|
||||||
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
|
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
|
||||||
|
await _writeProgramExerciseHealthServicesStrategySnapshot(
|
||||||
|
database,
|
||||||
|
exercise,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await database
|
await database
|
||||||
.into(database.workoutTemplates)
|
.into(database.workoutTemplates)
|
||||||
@ -523,6 +527,10 @@ final class DriftLocalSyncChangeRepository
|
|||||||
await database
|
await database
|
||||||
.into(database.programExercises)
|
.into(database.programExercises)
|
||||||
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
|
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
|
||||||
|
await _writeProgramExerciseHealthServicesStrategySnapshot(
|
||||||
|
database,
|
||||||
|
exercise,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
@ -996,7 +1004,7 @@ final class DriftProgramRepository implements ProgramRepository {
|
|||||||
.get();
|
.get();
|
||||||
return _programFromRow(
|
return _programFromRow(
|
||||||
row,
|
row,
|
||||||
exercises.map(_programExerciseFromRow).toList(),
|
await _programExercisesFromRows(database, exercises),
|
||||||
isExample: await _isProgramExample(id),
|
isExample: await _isProgramExample(id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1021,7 +1029,7 @@ final class DriftProgramRepository implements ProgramRepository {
|
|||||||
programs.add(
|
programs.add(
|
||||||
_programFromRow(
|
_programFromRow(
|
||||||
row,
|
row,
|
||||||
exercises.map(_programExerciseFromRow).toList(),
|
await _programExercisesFromRows(database, exercises),
|
||||||
isExample: await _isProgramExample(row.id),
|
isExample: await _isProgramExample(row.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -1055,9 +1063,15 @@ final class DriftProgramRepository implements ProgramRepository {
|
|||||||
tableName: 'program_exercises',
|
tableName: 'program_exercises',
|
||||||
entityType: 'ProgramExercise',
|
entityType: 'ProgramExercise',
|
||||||
metadata: exercise.metadata,
|
metadata: exercise.metadata,
|
||||||
write: () => database
|
write: () async {
|
||||||
.into(database.programExercises)
|
await database
|
||||||
.insertOnConflictUpdate(_programExerciseCompanion(exercise)),
|
.into(database.programExercises)
|
||||||
|
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
|
||||||
|
await _writeProgramExerciseHealthServicesStrategySnapshot(
|
||||||
|
database,
|
||||||
|
exercise,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3461,6 +3475,23 @@ Future<void> _writeExerciseStarterMetadata(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _writeProgramExerciseHealthServicesStrategySnapshot(
|
||||||
|
db.AppDatabase database,
|
||||||
|
domain.ProgramExercise exercise,
|
||||||
|
) async {
|
||||||
|
await database.customStatement(
|
||||||
|
'UPDATE program_exercises SET '
|
||||||
|
'health_services_exercise_type_strategy_snapshot_json = ? '
|
||||||
|
'WHERE id = ?',
|
||||||
|
[
|
||||||
|
_encodeHealthServicesExerciseTypes(
|
||||||
|
exercise.healthServicesExerciseTypeStrategySnapshot,
|
||||||
|
),
|
||||||
|
exercise.metadata.id,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _writeProgramStarterMetadata(
|
Future<void> _writeProgramStarterMetadata(
|
||||||
db.AppDatabase database,
|
db.AppDatabase database,
|
||||||
domain.Program program,
|
domain.Program program,
|
||||||
@ -4083,7 +4114,21 @@ db.ProgramExercisesCompanion _programExerciseCompanion(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
|
Future<List<domain.ProgramExercise>> _programExercisesFromRows(
|
||||||
|
db.AppDatabase database,
|
||||||
|
List<db.ProgramExercise> rows,
|
||||||
|
) async {
|
||||||
|
final output = <domain.ProgramExercise>[];
|
||||||
|
for (final row in rows) {
|
||||||
|
output.add(await _programExerciseFromRow(database, row));
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<domain.ProgramExercise> _programExerciseFromRow(
|
||||||
|
db.AppDatabase database,
|
||||||
|
db.ProgramExercise row,
|
||||||
|
) async {
|
||||||
return domain.ProgramExercise(
|
return domain.ProgramExercise(
|
||||||
metadata: _metadataFromRow(row),
|
metadata: _metadataFromRow(row),
|
||||||
programId: row.programId,
|
programId: row.programId,
|
||||||
@ -4102,6 +4147,8 @@ domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
|
|||||||
autoStartNextTimedStepSnapshot: row.autoStartNextTimedStepSnapshot,
|
autoStartNextTimedStepSnapshot: row.autoStartNextTimedStepSnapshot,
|
||||||
autoStartNextTimedStepOverride: row.autoStartNextTimedStepOverride,
|
autoStartNextTimedStepOverride: row.autoStartNextTimedStepOverride,
|
||||||
exerciseVideoMediaIdSnapshot: row.exerciseVideoMediaIdSnapshot,
|
exerciseVideoMediaIdSnapshot: row.exerciseVideoMediaIdSnapshot,
|
||||||
|
healthServicesExerciseTypeStrategySnapshot:
|
||||||
|
await _programExerciseHealthServicesStrategySnapshot(database, row.id),
|
||||||
exerciseArchivedSnapshot: row.exerciseArchivedSnapshot,
|
exerciseArchivedSnapshot: row.exerciseArchivedSnapshot,
|
||||||
availableTimeSnapshot: row.availableTimeSnapshot,
|
availableTimeSnapshot: row.availableTimeSnapshot,
|
||||||
availableRepsSnapshot: row.availableRepsSnapshot,
|
availableRepsSnapshot: row.availableRepsSnapshot,
|
||||||
@ -4121,6 +4168,25 @@ domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<domain.HealthServicesExerciseType>>
|
||||||
|
_programExerciseHealthServicesStrategySnapshot(
|
||||||
|
db.AppDatabase database,
|
||||||
|
String id,
|
||||||
|
) async {
|
||||||
|
final row = await database
|
||||||
|
.customSelect(
|
||||||
|
'SELECT health_services_exercise_type_strategy_snapshot_json '
|
||||||
|
'FROM program_exercises WHERE id = ? LIMIT 1',
|
||||||
|
variables: [Variable<String>(id)],
|
||||||
|
)
|
||||||
|
.getSingleOrNull();
|
||||||
|
return _decodeHealthServicesExerciseTypesJson(
|
||||||
|
row?.readNullable<String>(
|
||||||
|
'health_services_exercise_type_strategy_snapshot_json',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
db.WorkoutTemplatesCompanion _workoutTemplateCompanion(
|
db.WorkoutTemplatesCompanion _workoutTemplateCompanion(
|
||||||
domain.WorkoutTemplate template,
|
domain.WorkoutTemplate template,
|
||||||
) {
|
) {
|
||||||
@ -5802,6 +5868,10 @@ List<domain.ProgramExercise> _programExercisesFromPayload(
|
|||||||
),
|
),
|
||||||
exerciseVideoMediaIdSnapshot:
|
exerciseVideoMediaIdSnapshot:
|
||||||
map['exerciseVideoMediaIdSnapshot'] as String?,
|
map['exerciseVideoMediaIdSnapshot'] as String?,
|
||||||
|
healthServicesExerciseTypeStrategySnapshot:
|
||||||
|
_healthServicesExerciseTypesFromPayload(
|
||||||
|
map['healthServicesExerciseTypeStrategy'],
|
||||||
|
),
|
||||||
exerciseStepsSnapshot: _stepsFromPayload(
|
exerciseStepsSnapshot: _stepsFromPayload(
|
||||||
map['exerciseStepsSnapshot'],
|
map['exerciseStepsSnapshot'],
|
||||||
),
|
),
|
||||||
@ -6069,6 +6139,12 @@ String _encodeBusinessExerciseTypes(List<domain.BusinessExerciseType> types) {
|
|||||||
return jsonEncode(types.map(_businessExerciseTypeToDb).toList());
|
return jsonEncode(types.map(_businessExerciseTypeToDb).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _encodeHealthServicesExerciseTypes(
|
||||||
|
List<domain.HealthServicesExerciseType> types,
|
||||||
|
) {
|
||||||
|
return jsonEncode(types.map((type) => type.wireName).toList());
|
||||||
|
}
|
||||||
|
|
||||||
List<domain.BusinessExerciseType> _decodeBusinessExerciseTypesJson(
|
List<domain.BusinessExerciseType> _decodeBusinessExerciseTypesJson(
|
||||||
String? encoded,
|
String? encoded,
|
||||||
) {
|
) {
|
||||||
@ -6083,6 +6159,20 @@ List<domain.BusinessExerciseType> _decodeBusinessExerciseTypesJson(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<domain.HealthServicesExerciseType> _decodeHealthServicesExerciseTypesJson(
|
||||||
|
String? encoded,
|
||||||
|
) {
|
||||||
|
if (encoded == null || encoded.trim().isEmpty) {
|
||||||
|
return domain.legacyHealthServicesExerciseTypeStrategy;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(encoded);
|
||||||
|
return _healthServicesExerciseTypesFromPayload(decoded);
|
||||||
|
} on Object {
|
||||||
|
return domain.legacyHealthServicesExerciseTypeStrategy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<domain.BusinessExerciseType> _businessExerciseTypesFromPayload(
|
List<domain.BusinessExerciseType> _businessExerciseTypesFromPayload(
|
||||||
Object? value,
|
Object? value,
|
||||||
) {
|
) {
|
||||||
@ -6099,6 +6189,24 @@ List<domain.BusinessExerciseType> _businessExerciseTypesFromPayload(
|
|||||||
return List.unmodifiable(output);
|
return List.unmodifiable(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<domain.HealthServicesExerciseType> _healthServicesExerciseTypesFromPayload(
|
||||||
|
Object? value,
|
||||||
|
) {
|
||||||
|
if (value is! List) {
|
||||||
|
return domain.legacyHealthServicesExerciseTypeStrategy;
|
||||||
|
}
|
||||||
|
final output = <domain.HealthServicesExerciseType>[];
|
||||||
|
for (final raw in value.whereType<String>()) {
|
||||||
|
final type = _healthServicesExerciseTypeFromDbOrNull(raw);
|
||||||
|
if (type != null && !output.contains(type)) {
|
||||||
|
output.add(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output.isEmpty
|
||||||
|
? domain.legacyHealthServicesExerciseTypeStrategy
|
||||||
|
: List.unmodifiable(output);
|
||||||
|
}
|
||||||
|
|
||||||
List<domain.ExerciseStep> _stepsFromPayload(Object? value) {
|
List<domain.ExerciseStep> _stepsFromPayload(Object? value) {
|
||||||
if (value is! List) {
|
if (value is! List) {
|
||||||
return const [];
|
return const [];
|
||||||
@ -6364,6 +6472,17 @@ domain.BusinessExerciseType? _businessExerciseTypeFromDbOrNull(String value) =>
|
|||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
domain.HealthServicesExerciseType? _healthServicesExerciseTypeFromDbOrNull(
|
||||||
|
String value,
|
||||||
|
) => switch (value) {
|
||||||
|
'RUNNING' => domain.HealthServicesExerciseType.running,
|
||||||
|
'WALKING' => domain.HealthServicesExerciseType.walking,
|
||||||
|
'HIGH_INTENSITY_INTERVAL_TRAINING' =>
|
||||||
|
domain.HealthServicesExerciseType.highIntensityIntervalTraining,
|
||||||
|
'WORKOUT' => domain.HealthServicesExerciseType.workout,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
String _setResultStatusToDb(domain.SetResultStatus status) => switch (status) {
|
String _setResultStatusToDb(domain.SetResultStatus status) => switch (status) {
|
||||||
domain.SetResultStatus.completed => 'completed',
|
domain.SetResultStatus.completed => 'completed',
|
||||||
domain.SetResultStatus.skipped => 'skipped',
|
domain.SetResultStatus.skipped => 'skipped',
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
const int watchBridgeSchemaVersion = 6;
|
const int watchBridgeSchemaVersion = 7;
|
||||||
|
|
||||||
enum WatchCommandType {
|
enum WatchCommandType {
|
||||||
startCurrentExercise,
|
startCurrentExercise,
|
||||||
@ -300,6 +300,7 @@ final class WatchSessionProjection {
|
|||||||
this.manualScoreTargetLabel,
|
this.manualScoreTargetLabel,
|
||||||
this.manualScoreRepsTargetValue,
|
this.manualScoreRepsTargetValue,
|
||||||
this.manualScoreScope,
|
this.manualScoreScope,
|
||||||
|
this.healthServicesExerciseTypeStrategy = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
factory WatchSessionProjection.fromJson(Map<String, Object?> json) {
|
factory WatchSessionProjection.fromJson(Map<String, Object?> json) {
|
||||||
@ -362,6 +363,9 @@ final class WatchSessionProjection {
|
|||||||
json['manualScoreScope'],
|
json['manualScoreScope'],
|
||||||
WatchManualScoreScope.values,
|
WatchManualScoreScope.values,
|
||||||
),
|
),
|
||||||
|
healthServicesExerciseTypeStrategy: _stringListFromJson(
|
||||||
|
json['healthServicesExerciseTypeStrategy'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,6 +402,7 @@ final class WatchSessionProjection {
|
|||||||
final String? manualScoreTargetLabel;
|
final String? manualScoreTargetLabel;
|
||||||
final int? manualScoreRepsTargetValue;
|
final int? manualScoreRepsTargetValue;
|
||||||
final WatchManualScoreScope? manualScoreScope;
|
final WatchManualScoreScope? manualScoreScope;
|
||||||
|
final List<String> healthServicesExerciseTypeStrategy;
|
||||||
|
|
||||||
Map<String, Object?> toJson() {
|
Map<String, Object?> toJson() {
|
||||||
return {
|
return {
|
||||||
@ -438,6 +443,7 @@ final class WatchSessionProjection {
|
|||||||
'manualScoreTargetLabel': manualScoreTargetLabel,
|
'manualScoreTargetLabel': manualScoreTargetLabel,
|
||||||
'manualScoreRepsTargetValue': manualScoreRepsTargetValue,
|
'manualScoreRepsTargetValue': manualScoreRepsTargetValue,
|
||||||
'manualScoreScope': manualScoreScope?.name,
|
'manualScoreScope': manualScoreScope?.name,
|
||||||
|
'healthServicesExerciseTypeStrategy': healthServicesExerciseTypeStrategy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,7 +483,11 @@ final class WatchSessionProjection {
|
|||||||
manualScoreTargetValue == other.manualScoreTargetValue &&
|
manualScoreTargetValue == other.manualScoreTargetValue &&
|
||||||
manualScoreTargetLabel == other.manualScoreTargetLabel &&
|
manualScoreTargetLabel == other.manualScoreTargetLabel &&
|
||||||
manualScoreRepsTargetValue == other.manualScoreRepsTargetValue &&
|
manualScoreRepsTargetValue == other.manualScoreRepsTargetValue &&
|
||||||
manualScoreScope == other.manualScoreScope;
|
manualScoreScope == other.manualScoreScope &&
|
||||||
|
_listEquals(
|
||||||
|
healthServicesExerciseTypeStrategy,
|
||||||
|
other.healthServicesExerciseTypeStrategy,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -516,6 +526,7 @@ final class WatchSessionProjection {
|
|||||||
manualScoreTargetLabel,
|
manualScoreTargetLabel,
|
||||||
manualScoreRepsTargetValue,
|
manualScoreRepsTargetValue,
|
||||||
manualScoreScope,
|
manualScoreScope,
|
||||||
|
Object.hashAll(healthServicesExerciseTypeStrategy),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -886,6 +897,13 @@ String? _nullableStringFromJson(Object? value) {
|
|||||||
return value is String ? value : null;
|
return value is String ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> _stringListFromJson(Object? value) {
|
||||||
|
if (value is! List) {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
return value.whereType<String>().toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
int _intFromJson(Object? value, int fallback) {
|
int _intFromJson(Object? value, int fallback) {
|
||||||
return value is int ? value : fallback;
|
return value is int ? value : fallback;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -227,6 +227,11 @@ void main() {
|
|||||||
manualScoreTargetLabel: 'Cible',
|
manualScoreTargetLabel: 'Cible',
|
||||||
manualScoreRepsTargetValue: 12,
|
manualScoreRepsTargetValue: 12,
|
||||||
manualScoreScope: WatchManualScoreScope.step,
|
manualScoreScope: WatchManualScoreScope.step,
|
||||||
|
healthServicesExerciseTypeStrategy: const [
|
||||||
|
'RUNNING',
|
||||||
|
'HIGH_INTENSITY_INTERVAL_TRAINING',
|
||||||
|
'WORKOUT',
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
final decoded = WatchSessionProjection.fromJson(
|
final decoded = WatchSessionProjection.fromJson(
|
||||||
@ -274,6 +279,7 @@ void main() {
|
|||||||
expect(projection.manualScoreTargetLabel, isNull);
|
expect(projection.manualScoreTargetLabel, isNull);
|
||||||
expect(projection.manualScoreRepsTargetValue, isNull);
|
expect(projection.manualScoreRepsTargetValue, isNull);
|
||||||
expect(projection.manualScoreScope, isNull);
|
expect(projection.manualScoreScope, isNull);
|
||||||
|
expect(projection.healthServicesExerciseTypeStrategy, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('falls back to neutral values for absent required fields', () {
|
test('falls back to neutral values for absent required fields', () {
|
||||||
@ -300,6 +306,7 @@ void main() {
|
|||||||
expect(projection.manualScoreTargetLabel, isNull);
|
expect(projection.manualScoreTargetLabel, isNull);
|
||||||
expect(projection.manualScoreRepsTargetValue, isNull);
|
expect(projection.manualScoreRepsTargetValue, isNull);
|
||||||
expect(projection.manualScoreScope, isNull);
|
expect(projection.manualScoreScope, isNull);
|
||||||
|
expect(projection.healthServicesExerciseTypeStrategy, isEmpty);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,29 @@ void main() {
|
|||||||
expect(projection.dominantTimer, isNull);
|
expect(projection.dominantTimer, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'projects Health Services exercise type strategy from snapshot',
|
||||||
|
() async {
|
||||||
|
final repository = _FakeActiveSessionRepository()
|
||||||
|
..session = _session(
|
||||||
|
healthServicesExerciseTypeStrategy: const [
|
||||||
|
'RUNNING',
|
||||||
|
'HIGH_INTENSITY_INTERVAL_TRAINING',
|
||||||
|
'WORKOUT',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
final projector = _projector(repository, _clock());
|
||||||
|
|
||||||
|
final projection = await projector.project(revision: 1);
|
||||||
|
|
||||||
|
expect(projection.healthServicesExerciseTypeStrategy, [
|
||||||
|
'RUNNING',
|
||||||
|
'HIGH_INTENSITY_INTERVAL_TRAINING',
|
||||||
|
'WORKOUT',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('projects running with step timer before stopwatch score', () async {
|
test('projects running with step timer before stopwatch score', () async {
|
||||||
final now = DateTime.utc(2026, 7, 25, 12);
|
final now = DateTime.utc(2026, 7, 25, 12);
|
||||||
final session = _session(
|
final session = _session(
|
||||||
@ -587,6 +610,7 @@ ActiveWorkoutSession _session({
|
|||||||
bool? autoStartNextTimedStepSnapshot = true,
|
bool? autoStartNextTimedStepSnapshot = true,
|
||||||
List<ExerciseStep> steps = const [],
|
List<ExerciseStep> steps = const [],
|
||||||
String? secondExerciseName,
|
String? secondExerciseName,
|
||||||
|
List<String> healthServicesExerciseTypeStrategy = const [],
|
||||||
}) {
|
}) {
|
||||||
final exerciseSnapshot = {
|
final exerciseSnapshot = {
|
||||||
'id': 'exercise-snapshot-1',
|
'id': 'exercise-snapshot-1',
|
||||||
@ -603,6 +627,7 @@ ActiveWorkoutSession _session({
|
|||||||
.map((step) => step.toSnapshotJson())
|
.map((step) => step.toSnapshotJson())
|
||||||
.toList(),
|
.toList(),
|
||||||
'autoStartNextTimedStepSnapshot': ?autoStartNextTimedStepSnapshot,
|
'autoStartNextTimedStepSnapshot': ?autoStartNextTimedStepSnapshot,
|
||||||
|
'healthServicesExerciseTypeStrategy': healthServicesExerciseTypeStrategy,
|
||||||
};
|
};
|
||||||
final secondExerciseSnapshot = secondExerciseName == null
|
final secondExerciseSnapshot = secondExerciseName == null
|
||||||
? null
|
? null
|
||||||
|
|||||||
@ -125,6 +125,10 @@ void main() {
|
|||||||
|
|
||||||
expect(await columnNames('exercises'), contains('tags_json'));
|
expect(await columnNames('exercises'), contains('tags_json'));
|
||||||
expect(await columnNames('exercises'), contains('business_types_json'));
|
expect(await columnNames('exercises'), contains('business_types_json'));
|
||||||
|
expect(
|
||||||
|
await columnNames('program_exercises'),
|
||||||
|
contains('health_services_exercise_type_strategy_snapshot_json'),
|
||||||
|
);
|
||||||
expect(await columnNames('programs'), contains('tags_json'));
|
expect(await columnNames('programs'), contains('tags_json'));
|
||||||
expect(await columnNames('workout_templates'), contains('tags_json'));
|
expect(await columnNames('workout_templates'), contains('tags_json'));
|
||||||
expect(
|
expect(
|
||||||
@ -175,6 +179,48 @@ void main() {
|
|||||||
BusinessExerciseType.dribble,
|
BusinessExerciseType.dribble,
|
||||||
BusinessExerciseType.highIntensity,
|
BusinessExerciseType.highIntensity,
|
||||||
]);
|
]);
|
||||||
|
expect(restoredExercise?.healthServicesExerciseTypeStrategy, [
|
||||||
|
HealthServicesExerciseType.running,
|
||||||
|
HealthServicesExerciseType.highIntensityIntervalTraining,
|
||||||
|
HealthServicesExerciseType.workout,
|
||||||
|
]);
|
||||||
|
|
||||||
|
final program = Program(
|
||||||
|
metadata: _metadata('program-business-types', now),
|
||||||
|
name: 'Programme types',
|
||||||
|
defaultRestSeconds: 30,
|
||||||
|
exercises: [
|
||||||
|
ProgramExercise.snapshotFromExercise(
|
||||||
|
metadata: _metadata('program-exercise-business-types', now),
|
||||||
|
programId: 'program-business-types',
|
||||||
|
exercise: restoredExercise!,
|
||||||
|
position: 0,
|
||||||
|
setsCount: 1,
|
||||||
|
enabledMeasures: const {WorkoutMeasure.time},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
await programRepository.save(program);
|
||||||
|
|
||||||
|
final restoredProgram = await programRepository.findById(
|
||||||
|
program.metadata.id,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
restoredProgram
|
||||||
|
?.exercises
|
||||||
|
.single
|
||||||
|
.healthServicesExerciseTypeStrategySnapshot,
|
||||||
|
[
|
||||||
|
HealthServicesExerciseType.running,
|
||||||
|
HealthServicesExerciseType.highIntensityIntervalTraining,
|
||||||
|
HealthServicesExerciseType.workout,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
restoredProgram?.exercises.single
|
||||||
|
.toSnapshotJson()['healthServicesExerciseTypeStrategy'],
|
||||||
|
['RUNNING', 'HIGH_INTENSITY_INTERVAL_TRAINING', 'WORKOUT'],
|
||||||
|
);
|
||||||
|
|
||||||
final legacyExercise = Exercise(
|
final legacyExercise = Exercise(
|
||||||
metadata: _metadata('exercise-business-types-legacy', now),
|
metadata: _metadata('exercise-business-types-legacy', now),
|
||||||
@ -193,6 +239,12 @@ void main() {
|
|||||||
expect(restoredLegacy?.effectiveBusinessTypes, [
|
expect(restoredLegacy?.effectiveBusinessTypes, [
|
||||||
BusinessExerciseType.shoot,
|
BusinessExerciseType.shoot,
|
||||||
]);
|
]);
|
||||||
|
expect(restoredLegacy?.healthServicesExerciseTypeStrategy, [
|
||||||
|
HealthServicesExerciseType.highIntensityIntervalTraining,
|
||||||
|
HealthServicesExerciseType.running,
|
||||||
|
HealthServicesExerciseType.walking,
|
||||||
|
HealthServicesExerciseType.workout,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
@ -595,6 +647,23 @@ CREATE TABLE pending_share_actions (
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
await exerciseRepository.save(updatedExercise);
|
await exerciseRepository.save(updatedExercise);
|
||||||
|
await programRepository.save(
|
||||||
|
Program(
|
||||||
|
metadata: _metadata('program-sync-business-types', now),
|
||||||
|
name: 'Program business types',
|
||||||
|
defaultRestSeconds: 45,
|
||||||
|
exercises: [
|
||||||
|
ProgramExercise.snapshotFromExercise(
|
||||||
|
metadata: _metadata('program-exercise-sync-business-types', now),
|
||||||
|
programId: 'program-sync-business-types',
|
||||||
|
exercise: updatedExercise,
|
||||||
|
position: 0,
|
||||||
|
setsCount: 1,
|
||||||
|
enabledMeasures: const {WorkoutMeasure.time},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final changes = await syncChangeRepository.listPendingChanges();
|
final changes = await syncChangeRepository.listPendingChanges();
|
||||||
final payloadsById = {
|
final payloadsById = {
|
||||||
@ -605,6 +674,13 @@ CREATE TABLE pending_share_actions (
|
|||||||
'dribble',
|
'dribble',
|
||||||
'finishing',
|
'finishing',
|
||||||
]);
|
]);
|
||||||
|
final programExercises =
|
||||||
|
payloadsById['program-sync-business-types']!['exercises'] as List;
|
||||||
|
expect(programExercises.single['healthServicesExerciseTypeStrategy'], [
|
||||||
|
'RUNNING',
|
||||||
|
'HIGH_INTENSITY_INTERVAL_TRAINING',
|
||||||
|
'WORKOUT',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('local sync payload includes full workout history aggregate', () async {
|
test('local sync payload includes full workout history aggregate', () async {
|
||||||
|
|||||||
@ -597,6 +597,8 @@ object WatchBridgePlugin {
|
|||||||
"setIndex" to projection["setIndex"],
|
"setIndex" to projection["setIndex"],
|
||||||
"passageIndex" to zeroBasedNullableIndex(projection["passageIndex"]),
|
"passageIndex" to zeroBasedNullableIndex(projection["passageIndex"]),
|
||||||
"stepIndex" to zeroBasedNullableIndex(projection["stepIndex"]),
|
"stepIndex" to zeroBasedNullableIndex(projection["stepIndex"]),
|
||||||
|
"healthServicesExerciseTypeStrategy" to
|
||||||
|
projection["healthServicesExerciseTypeStrategy"],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -312,7 +312,13 @@ internal class WatchHeartRateCollector(
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
val capabilities = capabilitiesFuture.get()
|
val capabilities = capabilitiesFuture.get()
|
||||||
val config = exerciseConfigFromCapabilities(capabilities)
|
val requestedTypeNames = (
|
||||||
|
executionContext["healthServicesExerciseTypeStrategy"] as? List<*>
|
||||||
|
)?.filterIsInstance<String>().orEmpty()
|
||||||
|
val config = exerciseConfigFromCapabilities(
|
||||||
|
capabilities,
|
||||||
|
requestedTypeNames,
|
||||||
|
)
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
exerciseMetricsStartInFlight = false
|
exerciseMetricsStartInFlight = false
|
||||||
Log.w(TAG, "no exercise type supports heart rate or distance sessionId=$sessionId")
|
Log.w(TAG, "no exercise type supports heart rate or distance sessionId=$sessionId")
|
||||||
@ -360,13 +366,11 @@ internal class WatchHeartRateCollector(
|
|||||||
|
|
||||||
private fun exerciseConfigFromCapabilities(
|
private fun exerciseConfigFromCapabilities(
|
||||||
capabilities: androidx.health.services.client.data.ExerciseCapabilities,
|
capabilities: androidx.health.services.client.data.ExerciseCapabilities,
|
||||||
|
requestedTypeNames: List<String>,
|
||||||
): ExerciseConfig? {
|
): ExerciseConfig? {
|
||||||
val requestedTypes = listOf(
|
val requestedTypes = exerciseTypesFromNames(requestedTypeNames)
|
||||||
ExerciseType.RUNNING,
|
var distanceOnlyConfig: ExerciseConfig? = null
|
||||||
ExerciseType.WALKING,
|
var caloriesOnlyConfig: ExerciseConfig? = null
|
||||||
ExerciseType.HIGH_INTENSITY_INTERVAL_TRAINING,
|
|
||||||
ExerciseType.WORKOUT,
|
|
||||||
)
|
|
||||||
var heartRateOnlyConfig: ExerciseConfig? = null
|
var heartRateOnlyConfig: ExerciseConfig? = null
|
||||||
for (exerciseType in requestedTypes) {
|
for (exerciseType in requestedTypes) {
|
||||||
if (exerciseType !in capabilities.supportedExerciseTypes) {
|
if (exerciseType !in capabilities.supportedExerciseTypes) {
|
||||||
@ -376,15 +380,29 @@ internal class WatchHeartRateCollector(
|
|||||||
.supportedDataTypes
|
.supportedDataTypes
|
||||||
val dataTypes = mutableSetOf<androidx.health.services.client.data.DataType<*, *>>()
|
val dataTypes = mutableSetOf<androidx.health.services.client.data.DataType<*, *>>()
|
||||||
if (DataType.DISTANCE !in supported) {
|
if (DataType.DISTANCE !in supported) {
|
||||||
if (DataType.HEART_RATE_BPM !in supported) {
|
if (DataType.CALORIES in supported) {
|
||||||
|
dataTypes.add(DataType.CALORIES)
|
||||||
|
}
|
||||||
|
if (DataType.HEART_RATE_BPM in supported) {
|
||||||
|
dataTypes.add(DataType.HEART_RATE_BPM)
|
||||||
|
}
|
||||||
|
if (dataTypes.isEmpty()) {
|
||||||
Log.w(
|
Log.w(
|
||||||
TAG,
|
TAG,
|
||||||
"exercise type lacks distance and heart rate type=$exerciseType supported=$supported",
|
"exercise type lacks usable metrics type=$exerciseType supported=$supported",
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dataTypes.add(DataType.HEART_RATE_BPM)
|
if (
|
||||||
if (heartRateOnlyConfig == null) {
|
DataType.CALORIES in dataTypes &&
|
||||||
|
caloriesOnlyConfig == null
|
||||||
|
) {
|
||||||
|
caloriesOnlyConfig = ExerciseConfig.builder(exerciseType)
|
||||||
|
.setDataTypes(dataTypes)
|
||||||
|
.setIsAutoPauseAndResumeEnabled(false)
|
||||||
|
.setIsGpsEnabled(false)
|
||||||
|
.build()
|
||||||
|
} else if (heartRateOnlyConfig == null) {
|
||||||
heartRateOnlyConfig = ExerciseConfig.builder(exerciseType)
|
heartRateOnlyConfig = ExerciseConfig.builder(exerciseType)
|
||||||
.setDataTypes(dataTypes)
|
.setDataTypes(dataTypes)
|
||||||
.setIsAutoPauseAndResumeEnabled(false)
|
.setIsAutoPauseAndResumeEnabled(false)
|
||||||
@ -400,13 +418,44 @@ internal class WatchHeartRateCollector(
|
|||||||
if (DataType.HEART_RATE_BPM in supported) {
|
if (DataType.HEART_RATE_BPM in supported) {
|
||||||
dataTypes.add(DataType.HEART_RATE_BPM)
|
dataTypes.add(DataType.HEART_RATE_BPM)
|
||||||
}
|
}
|
||||||
return ExerciseConfig.builder(exerciseType)
|
val config = ExerciseConfig.builder(exerciseType)
|
||||||
.setDataTypes(dataTypes)
|
.setDataTypes(dataTypes)
|
||||||
.setIsAutoPauseAndResumeEnabled(false)
|
.setIsAutoPauseAndResumeEnabled(false)
|
||||||
.setIsGpsEnabled(true)
|
.setIsGpsEnabled(true)
|
||||||
.build()
|
.build()
|
||||||
|
if (DataType.CALORIES in dataTypes) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
if (distanceOnlyConfig == null) {
|
||||||
|
distanceOnlyConfig = config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return heartRateOnlyConfig
|
return distanceOnlyConfig ?: caloriesOnlyConfig ?: heartRateOnlyConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun exerciseTypesFromNames(names: List<String>): List<ExerciseType> {
|
||||||
|
val mapped = names.mapNotNull { name ->
|
||||||
|
when (name) {
|
||||||
|
"RUNNING" -> ExerciseType.RUNNING
|
||||||
|
"WALKING" -> ExerciseType.WALKING
|
||||||
|
"HIGH_INTENSITY_INTERVAL_TRAINING" ->
|
||||||
|
ExerciseType.HIGH_INTENSITY_INTERVAL_TRAINING
|
||||||
|
"WORKOUT" -> ExerciseType.WORKOUT
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
val fallback = listOf(
|
||||||
|
ExerciseType.RUNNING,
|
||||||
|
ExerciseType.WALKING,
|
||||||
|
ExerciseType.HIGH_INTENSITY_INTERVAL_TRAINING,
|
||||||
|
ExerciseType.WORKOUT,
|
||||||
|
)
|
||||||
|
for (exerciseType in fallback) {
|
||||||
|
if (exerciseType !in mapped) {
|
||||||
|
mapped.add(exerciseType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mapped.distinct()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopExerciseMetrics(context: Context) {
|
private fun stopExerciseMetrics(context: Context) {
|
||||||
|
|||||||
Reference in New Issue
Block a user