fix(watch): corrige la remontee distance/calories sur seance neuve dans WatchHeartRateCollector

QA vert cible. Complete le correctif precedent fix/distance-calories-health-services, insuffisant sur le cas seance neuve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 19:07:41 +02:00
parent 7e1458e36e
commit 078b742725

View File

@ -2,6 +2,8 @@ package com.gametime.watch.bridge
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Handler
import android.os.Looper
import android.util.Log
@ -315,11 +317,12 @@ internal class WatchHeartRateCollector(
val requestedTypeNames = (
executionContext["healthServicesExerciseTypeStrategy"] as? List<*>
)?.filterIsInstance<String>().orEmpty()
val config = exerciseConfigFromCapabilities(
val configs = exerciseConfigsFromCapabilities(
capabilities,
requestedTypeNames,
canUseGps = canStartGpsExercise(context),
)
if (config == null) {
if (configs.isEmpty()) {
exerciseMetricsStartInFlight = false
Log.w(TAG, "no exercise type supports usable live metrics sessionId=$sessionId")
if (shouldAggregate) {
@ -331,29 +334,11 @@ internal class WatchHeartRateCollector(
exerciseMetricsStartInFlight = false
return@addListener
}
exerciseHeartRateSupported = DataType.HEART_RATE_BPM in config.dataTypes
exerciseClient.setUpdateCallback(context.mainExecutor, exerciseCallback)
val startFuture = exerciseClient.startExerciseAsync(config)
startFuture.addListener(
{
exerciseMetricsStartInFlight = false
try {
startFuture.get()
exerciseMetricsStarted = true
Log.d(
TAG,
"exercise metrics started sessionId=$sessionId type=${config.exerciseType} dataTypes=${config.dataTypes}",
)
} catch (error: Exception) {
Log.w(TAG, "exercise metrics start failed", error)
clearExerciseCallback(exerciseClient)
exerciseHeartRateSupported = false
if (shouldAggregate) {
startMeasureHeartRateFallback(context)
}
}
},
context.mainExecutor,
startExerciseMetricsConfig(
context,
exerciseClient,
configs,
index = 0,
)
} catch (error: Exception) {
exerciseMetricsStartInFlight = false
@ -364,73 +349,144 @@ internal class WatchHeartRateCollector(
)
}
private fun exerciseConfigFromCapabilities(
private fun startExerciseMetricsConfig(
context: Context,
exerciseClient: ExerciseClient,
configs: List<ExerciseConfig>,
index: Int,
) {
if (!shouldAggregate) {
exerciseMetricsStartInFlight = false
return
}
val config = configs.getOrNull(index)
if (config == null) {
exerciseMetricsStartInFlight = false
clearExerciseCallback(exerciseClient)
exerciseHeartRateSupported = false
Log.w(TAG, "all exercise metric configs failed sessionId=$sessionId")
if (shouldAggregate) {
startMeasureHeartRateFallback(context)
}
return
}
exerciseHeartRateSupported = DataType.HEART_RATE_BPM in config.dataTypes
exerciseClient.setUpdateCallback(context.mainExecutor, exerciseCallback)
val startFuture = exerciseClient.startExerciseAsync(config)
startFuture.addListener(
{
try {
startFuture.get()
exerciseMetricsStartInFlight = false
exerciseMetricsStarted = true
Log.d(
TAG,
"exercise metrics started sessionId=$sessionId type=${config.exerciseType} dataTypes=${config.dataTypes}",
)
} catch (error: Exception) {
Log.w(
TAG,
"exercise metrics start failed type=${config.exerciseType} dataTypes=${config.dataTypes}",
error,
)
clearExerciseCallback(exerciseClient)
exerciseHeartRateSupported = false
startExerciseMetricsConfig(
context,
exerciseClient,
configs,
index + 1,
)
}
},
context.mainExecutor,
)
}
private fun exerciseConfigsFromCapabilities(
capabilities: androidx.health.services.client.data.ExerciseCapabilities,
requestedTypeNames: List<String>,
): ExerciseConfig? {
canUseGps: Boolean,
): List<ExerciseConfig> {
val requestedTypes = exerciseTypesFromNames(requestedTypeNames)
var distanceOnlyConfig: ExerciseConfig? = null
var caloriesOnlyConfig: ExerciseConfig? = null
var heartRateOnlyConfig: ExerciseConfig? = null
val distanceCaloriesConfigs = mutableListOf<ExerciseConfig>()
val caloriesConfigs = mutableListOf<ExerciseConfig>()
val heartRateConfigs = mutableListOf<ExerciseConfig>()
for (exerciseType in requestedTypes) {
if (exerciseType !in capabilities.supportedExerciseTypes) {
continue
}
val supported = capabilities.getExerciseTypeCapabilities(exerciseType)
.supportedDataTypes
val dataTypes = mutableSetOf<androidx.health.services.client.data.DataType<*, *>>()
if (DataType.DISTANCE !in supported) {
if (DataType.CALORIES in supported) {
dataTypes.add(DataType.CALORIES)
}
if (DataType.HEART_RATE_BPM in supported) {
val supportsHeartRate = DataType.HEART_RATE_BPM in supported
if (
canUseGps &&
DataType.DISTANCE in supported &&
DataType.CALORIES in supported
) {
val dataTypes = mutableSetOf<androidx.health.services.client.data.DataType<*, *>>(
DataType.DISTANCE,
DataType.CALORIES,
)
if (supportsHeartRate) {
dataTypes.add(DataType.HEART_RATE_BPM)
}
if (dataTypes.isEmpty()) {
Log.w(
TAG,
"exercise type lacks usable metrics type=$exerciseType supported=$supported",
)
continue
}
if (
DataType.CALORIES in dataTypes &&
caloriesOnlyConfig == null
) {
caloriesOnlyConfig = ExerciseConfig.builder(exerciseType)
distanceCaloriesConfigs.add(
ExerciseConfig.builder(exerciseType)
.setDataTypes(dataTypes)
.setIsAutoPauseAndResumeEnabled(false)
.setIsGpsEnabled(false)
.build()
} else if (heartRateOnlyConfig == null) {
heartRateOnlyConfig = ExerciseConfig.builder(exerciseType)
.setDataTypes(dataTypes)
.setIsAutoPauseAndResumeEnabled(false)
.setIsGpsEnabled(false)
.build()
}
continue
.setIsGpsEnabled(true)
.build(),
)
}
dataTypes.add(DataType.DISTANCE)
if (DataType.CALORIES in supported) {
dataTypes.add(DataType.CALORIES)
}
if (DataType.HEART_RATE_BPM in supported) {
dataTypes.add(DataType.HEART_RATE_BPM)
}
val config = ExerciseConfig.builder(exerciseType)
.setDataTypes(dataTypes)
.setIsAutoPauseAndResumeEnabled(false)
.setIsGpsEnabled(true)
.build()
if (DataType.CALORIES in dataTypes) {
return config
}
if (distanceOnlyConfig == null) {
distanceOnlyConfig = config
val dataTypes = mutableSetOf<androidx.health.services.client.data.DataType<*, *>>(
DataType.CALORIES,
)
if (supportsHeartRate) {
dataTypes.add(DataType.HEART_RATE_BPM)
}
caloriesConfigs.add(
ExerciseConfig.builder(exerciseType)
.setDataTypes(dataTypes)
.setIsAutoPauseAndResumeEnabled(false)
.setIsGpsEnabled(false)
.build(),
)
} else if (supportsHeartRate) {
heartRateConfigs.add(
ExerciseConfig.builder(exerciseType)
.setDataTypes(setOf(DataType.HEART_RATE_BPM))
.setIsAutoPauseAndResumeEnabled(false)
.setIsGpsEnabled(false)
.build(),
)
} else {
Log.w(
TAG,
"exercise type lacks usable metrics type=$exerciseType supported=$supported",
)
}
}
return caloriesOnlyConfig ?: distanceOnlyConfig ?: heartRateOnlyConfig
return distanceCaloriesConfigs + caloriesConfigs + heartRateConfigs
}
private fun canStartGpsExercise(context: Context): Boolean {
if (
context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) !=
PackageManager.PERMISSION_GRANTED
) {
return false
}
return try {
val locationManager = context.getSystemService(LocationManager::class.java)
?: return false
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
} catch (error: RuntimeException) {
Log.w(TAG, "location availability check failed", error)
false
}
}
private fun exerciseTypesFromNames(names: List<String>): List<ExerciseType> {