merge: integre fix/distance-calories-nouvelle-seance dans develop

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

View File

@ -2,6 +2,8 @@ package com.gametime.watch.bridge
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
@ -315,11 +317,12 @@ internal class WatchHeartRateCollector(
val requestedTypeNames = ( val requestedTypeNames = (
executionContext["healthServicesExerciseTypeStrategy"] as? List<*> executionContext["healthServicesExerciseTypeStrategy"] as? List<*>
)?.filterIsInstance<String>().orEmpty() )?.filterIsInstance<String>().orEmpty()
val config = exerciseConfigFromCapabilities( val configs = exerciseConfigsFromCapabilities(
capabilities, capabilities,
requestedTypeNames, requestedTypeNames,
canUseGps = canStartGpsExercise(context),
) )
if (config == null) { if (configs.isEmpty()) {
exerciseMetricsStartInFlight = false exerciseMetricsStartInFlight = false
Log.w(TAG, "no exercise type supports usable live metrics sessionId=$sessionId") Log.w(TAG, "no exercise type supports usable live metrics sessionId=$sessionId")
if (shouldAggregate) { if (shouldAggregate) {
@ -331,29 +334,11 @@ internal class WatchHeartRateCollector(
exerciseMetricsStartInFlight = false exerciseMetricsStartInFlight = false
return@addListener return@addListener
} }
exerciseHeartRateSupported = DataType.HEART_RATE_BPM in config.dataTypes startExerciseMetricsConfig(
exerciseClient.setUpdateCallback(context.mainExecutor, exerciseCallback) context,
val startFuture = exerciseClient.startExerciseAsync(config) exerciseClient,
startFuture.addListener( configs,
{ index = 0,
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,
) )
} catch (error: Exception) { } catch (error: Exception) {
exerciseMetricsStartInFlight = false 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, capabilities: androidx.health.services.client.data.ExerciseCapabilities,
requestedTypeNames: List<String>, requestedTypeNames: List<String>,
): ExerciseConfig? { canUseGps: Boolean,
): List<ExerciseConfig> {
val requestedTypes = exerciseTypesFromNames(requestedTypeNames) val requestedTypes = exerciseTypesFromNames(requestedTypeNames)
var distanceOnlyConfig: ExerciseConfig? = null val distanceCaloriesConfigs = mutableListOf<ExerciseConfig>()
var caloriesOnlyConfig: ExerciseConfig? = null val caloriesConfigs = mutableListOf<ExerciseConfig>()
var heartRateOnlyConfig: ExerciseConfig? = null val heartRateConfigs = mutableListOf<ExerciseConfig>()
for (exerciseType in requestedTypes) { for (exerciseType in requestedTypes) {
if (exerciseType !in capabilities.supportedExerciseTypes) { if (exerciseType !in capabilities.supportedExerciseTypes) {
continue continue
} }
val supported = capabilities.getExerciseTypeCapabilities(exerciseType) val supported = capabilities.getExerciseTypeCapabilities(exerciseType)
.supportedDataTypes .supportedDataTypes
val dataTypes = mutableSetOf<androidx.health.services.client.data.DataType<*, *>>() val supportsHeartRate = DataType.HEART_RATE_BPM in supported
if (DataType.DISTANCE !in supported) { if (
if (DataType.CALORIES in supported) { canUseGps &&
dataTypes.add(DataType.CALORIES) DataType.DISTANCE in supported &&
} DataType.CALORIES in supported
if (DataType.HEART_RATE_BPM in supported) { ) {
val dataTypes = mutableSetOf<androidx.health.services.client.data.DataType<*, *>>(
DataType.DISTANCE,
DataType.CALORIES,
)
if (supportsHeartRate) {
dataTypes.add(DataType.HEART_RATE_BPM) dataTypes.add(DataType.HEART_RATE_BPM)
} }
if (dataTypes.isEmpty()) { distanceCaloriesConfigs.add(
Log.w( ExerciseConfig.builder(exerciseType)
TAG,
"exercise type lacks usable metrics type=$exerciseType supported=$supported",
)
continue
}
if (
DataType.CALORIES in dataTypes &&
caloriesOnlyConfig == null
) {
caloriesOnlyConfig = ExerciseConfig.builder(exerciseType)
.setDataTypes(dataTypes) .setDataTypes(dataTypes)
.setIsAutoPauseAndResumeEnabled(false) .setIsAutoPauseAndResumeEnabled(false)
.setIsGpsEnabled(false) .setIsGpsEnabled(true)
.build() .build(),
} else if (heartRateOnlyConfig == null) { )
heartRateOnlyConfig = ExerciseConfig.builder(exerciseType)
.setDataTypes(dataTypes)
.setIsAutoPauseAndResumeEnabled(false)
.setIsGpsEnabled(false)
.build()
}
continue
} }
dataTypes.add(DataType.DISTANCE)
if (DataType.CALORIES in supported) { if (DataType.CALORIES in supported) {
dataTypes.add(DataType.CALORIES) val dataTypes = mutableSetOf<androidx.health.services.client.data.DataType<*, *>>(
} DataType.CALORIES,
if (DataType.HEART_RATE_BPM in supported) { )
dataTypes.add(DataType.HEART_RATE_BPM) if (supportsHeartRate) {
} dataTypes.add(DataType.HEART_RATE_BPM)
val config = ExerciseConfig.builder(exerciseType) }
.setDataTypes(dataTypes) caloriesConfigs.add(
.setIsAutoPauseAndResumeEnabled(false) ExerciseConfig.builder(exerciseType)
.setIsGpsEnabled(true) .setDataTypes(dataTypes)
.build() .setIsAutoPauseAndResumeEnabled(false)
if (DataType.CALORIES in dataTypes) { .setIsGpsEnabled(false)
return config .build(),
} )
if (distanceOnlyConfig == null) { } else if (supportsHeartRate) {
distanceOnlyConfig = config 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> { private fun exerciseTypesFromNames(names: List<String>): List<ExerciseType> {