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:
2026-07-30 16:17:31 +02:00
parent 30e657a67f
commit 2ae716206a
10 changed files with 444 additions and 22 deletions

View File

@ -597,6 +597,8 @@ object WatchBridgePlugin {
"setIndex" to projection["setIndex"],
"passageIndex" to zeroBasedNullableIndex(projection["passageIndex"]),
"stepIndex" to zeroBasedNullableIndex(projection["stepIndex"]),
"healthServicesExerciseTypeStrategy" to
projection["healthServicesExerciseTypeStrategy"],
)
}

View File

@ -312,7 +312,13 @@ internal class WatchHeartRateCollector(
{
try {
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) {
exerciseMetricsStartInFlight = false
Log.w(TAG, "no exercise type supports heart rate or distance sessionId=$sessionId")
@ -360,13 +366,11 @@ internal class WatchHeartRateCollector(
private fun exerciseConfigFromCapabilities(
capabilities: androidx.health.services.client.data.ExerciseCapabilities,
requestedTypeNames: List<String>,
): ExerciseConfig? {
val requestedTypes = listOf(
ExerciseType.RUNNING,
ExerciseType.WALKING,
ExerciseType.HIGH_INTENSITY_INTERVAL_TRAINING,
ExerciseType.WORKOUT,
)
val requestedTypes = exerciseTypesFromNames(requestedTypeNames)
var distanceOnlyConfig: ExerciseConfig? = null
var caloriesOnlyConfig: ExerciseConfig? = null
var heartRateOnlyConfig: ExerciseConfig? = null
for (exerciseType in requestedTypes) {
if (exerciseType !in capabilities.supportedExerciseTypes) {
@ -376,15 +380,29 @@ internal class WatchHeartRateCollector(
.supportedDataTypes
val dataTypes = mutableSetOf<androidx.health.services.client.data.DataType<*, *>>()
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(
TAG,
"exercise type lacks distance and heart rate type=$exerciseType supported=$supported",
"exercise type lacks usable metrics type=$exerciseType supported=$supported",
)
continue
}
dataTypes.add(DataType.HEART_RATE_BPM)
if (heartRateOnlyConfig == null) {
if (
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)
.setDataTypes(dataTypes)
.setIsAutoPauseAndResumeEnabled(false)
@ -400,13 +418,44 @@ internal class WatchHeartRateCollector(
if (DataType.HEART_RATE_BPM in supported) {
dataTypes.add(DataType.HEART_RATE_BPM)
}
return ExerciseConfig.builder(exerciseType)
val config = ExerciseConfig.builder(exerciseType)
.setDataTypes(dataTypes)
.setIsAutoPauseAndResumeEnabled(false)
.setIsGpsEnabled(true)
.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) {