Programmes d'entraînement

Santé Connect fournit un type de données exercice planifié pour permettre aux applications d'entraînement d'écrire des programmes d'entraînement et aux applications de remise en forme de lire ces programmes. Les exercices enregistrés (entraînements) peuvent être relus pour une analyse personnalisée des performances afin d'aider les utilisateurs à atteindre leurs objectifs d'entraînement.

Vérifier la disponibilité de Santé Connect

Avant de tenter d'utiliser Santé Connect, votre application doit vérifier que Santé Connect est disponible sur l'appareil de l'utilisateur. Santé Connect peut ne pas être préinstallé sur tous les appareils ou être désactivé. Vous pouvez vérifier la disponibilité à l'aide de la méthode HealthConnectClient.getSdkStatus().

Vérifier la disponibilité de Santé Connect

fun checkHealthConnectAvailability(context: Context) {
    val providerPackageName = "com.google.android.apps.healthdata" // Or get from HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME
    val availabilityStatus = HealthConnectClient.getSdkStatus(context, providerPackageName)

    if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE) {
      // Health Connect is not available. Guide the user to install/enable it.
      // For example, show a dialog.
      return // early return as there is no viable integration
    }
    if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
      // Health Connect is available but requires an update.
      // Optionally redirect to package installer to find a provider, for example:
      val uriString = "market://details?id=$providerPackageName&url=healthconnect%3A%2F%2Fonboarding"
      context.startActivity(
        Intent(Intent.ACTION_VIEW).apply {
          setPackage("com.android.vending")
          data = Uri.parse(uriString)
          putExtra("overlay", true)
          putExtra("callerId", context.packageName)
        }
      )
      return
    }
    // Health Connect is available, obtain a HealthConnectClient instance
    val healthConnectClient = HealthConnectClient.getOrCreate(context)
    // Issue operations with healthConnectClient
}

En fonction de l'état renvoyé par getSdkStatus(), vous pouvez inviter l'utilisateur à installer ou à mettre à jour Santé Connect depuis le Google Play Store si nécessaire.

Disponibilité de la fonctionnalité

Pour déterminer si l'appareil d'un utilisateur est compatible avec les programmes d'entraînement sur Santé Connect, vérifiez la disponibilité de FEATURE_PLANNED_EXERCISE sur le client :

if (healthConnectClient
     .features
     .getFeatureStatus(
       HealthConnectFeatures.FEATURE_PLANNED_EXERCISE
     ) == HealthConnectFeatures.FEATURE_STATUS_AVAILABLE) {

  // Feature is available
} else {
  // Feature isn't available
}
Pour en savoir plus, consultez Vérifier la disponibilité des fonctionnalités.

Autorisations requises

L'accès à l'exercice planifié est protégé par les autorisations suivantes :

  • android.permission.health.READ_PLANNED_EXERCISE
  • android.permission.health.WRITE_PLANNED_EXERCISE

Pour ajouter la fonctionnalité d'exercice planifié à votre application, commencez par demander des autorisations pour le type de données PlannedExerciseSession.

Voici l'autorisation que vous devez déclarer pour pouvoir écrire des exercices planifiés :

<application>
  <uses-permission
android:name="android.permission.health.WRITE_PLANNED_EXERCISE" />
...
</application>

Pour lire un exercice planifié, vous devez demander les autorisations suivantes :

<application>
  <uses-permission
android:name="android.permission.health.READ_PLANNED_EXERCISE" />
...
</application>

Demander des autorisations à l'utilisateur

Après avoir créé une instance client, votre application doit demander des autorisations à l'utilisateur. Les utilisateurs doivent être autorisés à accorder ou à refuser des autorisations à tout moment. Pour ce faire, créez un ensemble d'autorisations pour les types de données requis. Assurez-vous d'abord que les autorisations de l'ensemble sont déclarées dans votre fichier manifeste Android.

val permissions =
    setOf(
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getWritePermission(HeartRateRecord::class),
        HealthPermission.getReadPermission(PlannedExerciseSessionRecord::class),
        HealthPermission.getWritePermission(PlannedExerciseSessionRecord::class),
        HealthPermission.getReadPermission(ExerciseSessionRecord::class),
        HealthPermission.getWritePermission(ExerciseSessionRecord::class)
    )
Utilisez getGrantedPermissions pour voir si votre application dispose déjà des autorisations requises accordées. Si ce n'est pas le cas, utilisez createRequestPermissionResultContract pour demander ces autorisations. L'écran des autorisations de Santé Connect s'affiche.
val permissions = setOf(
        HealthPermission.getReadPermission(StepsRecord::class),
        HealthPermission.getWritePermission(StepsRecord::class),
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getWritePermission(HeartRateRecord::class)
    )

val requestPermissionsLauncher = rememberLauncherForActivityResult(
    contract = PermissionController.createRequestPermissionResultContract()
) { grantedPermissions ->
    if (grantedPermissions.containsAll(permissions)) {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions granted!") }
    } else {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions denied.") }
    }
}
Étant donné que les utilisateurs peuvent accorder ou révoquer des autorisations à tout moment, votre application doit vérifier les autorisations chaque fois avant de les utiliser et gérer les scénarios dans lesquels l'autorisation est perdue.

Les programmes d'entraînement sont liés aux sessions d'exercice. Par conséquent, l'utilisateur doit autoriser l'utilisation de chaque type d'enregistrement associé à un programme d'entraînement afin d'utiliser pleinement cette fonctionnalité de Santé Connect.

Par exemple, si un programme d'entraînement mesure la fréquence cardiaque d'un utilisateur lors d'une série de courses, le développeur devra peut-être déclarer les autorisations suivantes, que l'utilisateur devra accorder pour écrire la session d'exercice et lire les résultats en vue d'une évaluation ultérieure :

  • android.permission.health.READ_EXERCISE
  • android.permission.health.READ_EXERCISE_ROUTES
  • android.permission.health.READ_HEART_RATE
  • android.permission.health.WRITE_EXERCISE
  • android.permission.health.WRITE_EXERCISE_ROUTE
  • android.permission.health.WRITE_HEART_RATE

Toutefois, l'application qui crée des programmes d'entraînement et évalue les performances par rapport à ces programmes n'est souvent pas la même que celle qui consomme les programmes d'entraînement et écrit les données d'exercice réelles. Selon le type d'application, toutes les autorisations de lecture et d'écriture ne sont pas nécessaires. Par exemple, vous n'aurez peut-être besoin que de ces autorisations pour chaque type d'application :

Application de programme d'entraînement Application d'entraînement
WRITE_PLANNED_EXERCISE READ_PLANNED_EXERCISE
READ_EXERCISE WRITE_EXERCISE
READ_EXERCISE_ROUTES WRITE_EXERCISE_ROUTE
READ_HEART_RATE WRITE_HEART_RATE

Informations incluses dans un enregistrement de session d'exercice planifié

  • Titre de la session.
  • Liste des blocs d'exercices planifiés.
  • Heure de début et de fin de la session.
  • Type d'exercice.
  • Notes concernant l'activité.
  • Métadonnées.
  • ID de la session d'exercice terminée : cette valeur est écrite automatiquement une fois qu'une session d'exercice liée à cette session d'exercice planifiée est terminée.

Informations incluses dans un enregistrement de bloc d'exercice planifié

Un bloc d'exercice planifié contient une liste d'étapes d'exercice pour permettre la répétition de différents groupes d'étapes (par exemple, effectuer une séquence de flexions des bras, de burpees et de crunchs cinq fois de suite).

Informations incluses dans un enregistrement d'étape d'exercice planifiée

Agrégations compatibles

Aucune agrégation n'est compatible avec ce type de données.

Exemple d'utilisation

Supposons qu'un utilisateur planifie une course de 90 minutes dans deux jours. Cette course comprendra trois tours autour d'un lac avec une fréquence cardiaque cible comprise entre 90 et 110 bpm.

  1. Une session d'exercice planifiée avec les éléments suivants est définie par l'utilisateur dans une application de programme d'entraînement :
    1. Début et fin planifiés de la course
    2. Type d'exercice (course à pied)
    3. Nombre de tours (répétitions)
    4. Objectif de performances pour la fréquence cardiaque (entre 90 et 110 bpm)
  2. Ces informations sont regroupées en blocs et étapes d'exercice, puis écrites dans Santé Connect par l'application de programme d'entraînement en tant que PlannedExerciseSessionRecord.
  3. L'utilisateur effectue la session planifiée (course à pied).
  4. Les données d'exercice liées à la session sont enregistrées :
    1. Par un accessoire connecté pendant la session. Par exemple, la fréquence cardiaque. Ces données sont écrites dans Santé Connect en tant que type d'enregistrement pour l'activité. Dans ce cas, HeartRateRecord.
    2. Manuellement par l'utilisateur après la session. Par exemple, en indiquant le début et la fin de la course réelle. Ces données sont écrites dans Santé Connect en tant que ExerciseSessionRecord.
  5. Plus tard, l'application de programme d'entraînement lit les données de Santé Connect pour évaluer les performances réelles par rapport aux objectifs définis par l'utilisateur dans la session d'exercice planifiée.

Planifier des exercices et définir des objectifs

Un utilisateur peut planifier son exercice à l'avenir et définir des objectifs. Écrivez-le dans Santé Connect en tant que session d'exercice planifiée.

Dans l'exemple décrit dans Exemple d'utilisation, l'utilisateur planifie une course de 90 minutes dans deux jours. Cette course comprendra trois tours autour d'un lac avec une fréquence cardiaque cible comprise entre 90 et 110 bpm.

Un extrait de code comme celui-ci peut être trouvé dans le gestionnaire de formulaire d'une application qui enregistre des sessions d'exercice planifiées dans Santé Connect. Il peut également être trouvé dans le point d'ingestion des intégrations, par exemple avec un service proposant des formations.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()

if (!grantedPermissions.contains(
        HealthPermission.getWritePermission(PlannedExerciseSessionRecord::class))) {
    // The user hasn't granted the app permission to write planned exercise session data.
    Log.w("HealthConnect", "Write permission for PlannedExerciseSessionRecord not granted.")
    return
}

val plannedExerciseSessionRecord = PlannedExerciseSessionRecord(
    startTime = startTime,
    endTime = endTime,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    blocks = listOf(
        PlannedExerciseBlock(
            repetitions = 1, steps = listOf(
                PlannedExerciseStep(
                    exerciseType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
                    exercisePhase = PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
                    completionGoal = ExerciseCompletionGoal.RepetitionsGoal(repetitions = 3),
                    performanceTargets = listOf(
                        ExercisePerformanceTarget.HeartRateTarget(
                            minHeartRate = 90.0, maxHeartRate = 110.0
                        )
                    )
                ),
            ), description = "Three laps around the lake"
        )
    ),
    title = "Run at lake",
    notes = null,
    metadata = Metadata(
        device = Device(type = Device.Companion.TYPE_PHONE),
    ),
    startZoneOffset = null,
    endZoneOffset = null,
)

try {
    // Attempt to insert the record
    val response = healthConnectClient.insertRecords(listOf(plannedExerciseSessionRecord))

    // If execution reaches here, the insert succeeded.
    // Safely extract the ID using firstOrNull()
    val insertedPlannedExerciseSessionId = response.recordIdsList.firstOrNull()

    if (insertedPlannedExerciseSessionId != null) {
        Log.d("HealthConnect", "Successfully inserted planned exercise session ID: $insertedPlannedExerciseSessionId")
    } else {
        Log.w("HealthConnect", "Insertion succeeded but no record IDs were returned.")
    }

} catch (e: Exception) {
    // Handle API failures, database errors, or system issues safely without crashing
    Log.e("HealthConnect", "Failed to insert planned exercise session record", e)
}

Enregistrer les données d'exercice et d'activité

Deux jours plus tard, l'utilisateur enregistre la session d'exercice réelle. Écrivez-le dans Santé Connect en tant que session d'exercice.

Dans cet exemple, la durée de la session de l'utilisateur correspond exactement à la durée prévue.

L'extrait de code suivant peut être trouvé dans le gestionnaire de formulaire d'une application qui enregistre des sessions d'exercice dans Santé Connect. Il peut également être trouvé dans les gestionnaires d'ingestion et d'exportation de données pour un accessoire connecté capable de détecter et d'enregistrer des sessions d'exercice.

insertedPlannedExerciseSessionId est réutilisé ici à partir de l'exemple précédent. Dans une application réelle, l'ID serait déterminé par l'utilisateur qui sélectionne une session d'exercice planifiée dans une liste de sessions existantes.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()
if (!grantedPermissions.contains(
        HealthPermission.getWritePermission(ExerciseSessionRecord::class))) {
    // The user doesn't granted the app permission to write exercise session data.
    return
}

val sessionDuration = Duration.ofMinutes(90)
val sessionEndTime = Instant.now()
val sessionStartTime = sessionEndTime.minus(sessionDuration)

val exerciseSessionRecord = ExerciseSessionRecord(
    startTime = sessionStartTime,
    startZoneOffset = ZoneOffset.UTC,
    endTime = sessionEndTime,
    endZoneOffset = ZoneOffset.UTC,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    segments = listOf(
        ExerciseSegment(
            startTime = sessionStartTime,
            endTime = sessionEndTime,
            repetitions = 3,
            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING
        )
    ),
    title = "Run at lake",
    plannedExerciseSessionId = insertedPlannedExerciseSessionId,
    metadata = Metadata(
        device = Device(type = Device.Companion.TYPE_PHONE)
    )
)
val insertedExerciseSessions =
    healthConnectClient.insertRecords(listOf(exerciseSessionRecord))

Un accessoire connecté enregistre également sa fréquence cardiaque tout au long de la course. L'extrait de code suivant peut être utilisé pour générer des enregistrements dans la plage cible.

Dans une application réelle, les éléments principaux de cet extrait de code peuvent être trouvés dans le gestionnaire d'un message provenant d'un accessoire connecté, qui écrirait la mesure dans Santé Connect lors de la collecte.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()
if (!grantedPermissions.contains(
        HealthPermission.getWritePermission(HeartRateRecord::class))) {
    // The user doesn't granted the app permission to write heart rate record data.
    return
}

val samples = mutableListOf<HeartRateRecord.Sample>()
var currentTime = sessionStartTime
while (currentTime.isBefore(sessionEndTime)) {
    val bpm = Random.nextInt(21) + 90
    val heartRateRecord = HeartRateRecord.Sample(
        time = currentTime,
        beatsPerMinute = bpm.toLong(),
    )
    samples.add(heartRateRecord)
    currentTime = currentTime.plusSeconds(180)
}

val heartRateRecord = HeartRateRecord(
    startTime = sessionStartTime,
    startZoneOffset = ZoneOffset.UTC,
    endTime = sessionEndTime,
    endZoneOffset = ZoneOffset.UTC,
    samples = samples,
    metadata = Metadata(
        device = Device(type = Device.Companion.TYPE_WATCH)
    )
)
val insertedHeartRateRecords = healthConnectClient.insertRecords(listOf(heartRateRecord))

Évaluer les objectifs de performances

Le lendemain de l'entraînement de l'utilisateur, vous pouvez récupérer l'exercice enregistré, vérifier les objectifs d'exercice planifiés et évaluer d'autres types de données pour déterminer si les objectifs définis ont été atteints.

Un extrait de code comme celui-ci se trouve probablement dans une tâche périodique pour évaluer les objectifs de performances ou lors du chargement d'une liste d'exercices et de l'affichage d'une notification concernant les objectifs de performances dans une application.

    // Verify the user has granted all necessary permissions for this task
    val grantedPermissions =
        healthConnectClient.permissionController.getGrantedPermissions()
    if (!grantedPermissions.containsAll(
            listOf(
                HealthPermission.getReadPermission(ExerciseSessionRecord::class),
                HealthPermission.getReadPermission(PlannedExerciseSessionRecord::class),
                HealthPermission.getReadPermission(HeartRateRecord::class)
            )
        )
    ) {
        // The user doesn't granted the app permission to read exercise session record data.
        return
    }

    val searchDuration = Duration.ofDays(1)
    val searchEndTime = Instant.now()
    val searchStartTime = searchEndTime.minus(searchDuration)

    val response = healthConnectClient.readRecords(
        ReadRecordsRequest<ExerciseSessionRecord>(
            timeRangeFilter = TimeRangeFilter.between(searchStartTime, searchEndTime)
        )
    )
    for (exerciseRecord in response.records) {
        val plannedExerciseRecordId = exerciseRecord.plannedExerciseSessionId
        val plannedExerciseRecord =
            if (plannedExerciseRecordId == null) null else healthConnectClient.readRecord(
                PlannedExerciseSessionRecord::class, plannedExerciseRecordId
            ).record
        if (plannedExerciseRecord != null) {
            val aggregateRequest = AggregateRequest(
                metrics = setOf(HeartRateRecord.BPM_AVG),
                timeRangeFilter = TimeRangeFilter.between(
                    exerciseRecord.startTime, exerciseRecord.endTime
                ),
            )
            val aggregationResult = healthConnectClient.aggregate(aggregateRequest)

            val maxBpm = aggregationResult[HeartRateRecord.BPM_MAX]
            val minBpm = aggregationResult[HeartRateRecord.BPM_MIN]
            if (maxBpm != null && minBpm != null) {
                plannedExerciseRecord.blocks.forEach { block ->
                    block.steps.forEach { step ->
                        step.performanceTargets.forEach { target ->
                            when (target) {
                                is ExercisePerformanceTarget.HeartRateTarget -> {
                                    val minTarget = target.minHeartRate
                                    val maxTarget = target.maxHeartRate
                                    if(
                                        minBpm >= minTarget && maxBpm <= maxTarget
                                    ) {
                                        // Success!
                                    }
                                }
                                // Handle more target types
                            }
                        }
                    }
                }
            }
        }
    }
}

Sessions d'exercice

Les sessions d'exercice peuvent inclure toutes sortes d'activités, de la course à pied au badminton.

Écrire des sessions d'exercice

Voici comment créer une requête d'insertion incluant une session :

suspend fun writeExerciseSession(healthConnectClient: HealthConnectClient) {
    healthConnectClient.insertRecords(
        listOf(
            ExerciseSessionRecord(
                startTime = START_TIME,
                startZoneOffset = START_ZONE_OFFSET,
                endTime = END_TIME,
                endZoneOffset = END_ZONE_OFFSET,
                exerciseType = ExerciseSessionRecord.ExerciseType.RUNNING,
                title = "My Run",
                metadata = Metadata.manualEntry()
            ),
            // ... other records
        )
    )
}

Lire une session d'exercice

Voici un exemple de lecture d'une session d'exercice :

suspend fun readExerciseSessions(
    healthConnectClient: HealthConnectClient,
    startTime: Instant,
    endTime: Instant
) {
    val response =
        healthConnectClient.readRecords(
            ReadRecordsRequest(
                ExerciseSessionRecord::class,
                timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
            )
        )
    for (exerciseRecord in response.records) {
        // Process each exercise record
        // Optionally pull in with other data sources of the same time range.
        val distanceRecord =
            healthConnectClient
                .readRecords(
                    ReadRecordsRequest(
                        DistanceRecord::class,
                        timeRangeFilter =
                            TimeRangeFilter.between(
                                exerciseRecord.startTime,
                                exerciseRecord.endTime
                            )
                    )
                )
                .records
    }
}

Écrire des données de sous-type

Les sessions peuvent également être constituées de données de sous-type facultatives, qui enrichiront la session avec des informations supplémentaires.

Par exemple, les sessions d'exercice peuvent inclure les classes ExerciseSegment, ExerciseLap et ExerciseRoute :

val segments = listOf(
  ExerciseSegment(
    startTime = Instant.parse("2022-01-02T10:10:10Z"),
    endTime = Instant.parse("2022-01-02T10:10:13Z"),
    segmentType = ActivitySegmentType.BENCH_PRESS,
    repetitions = 373
  )
)

val laps = listOf(
  ExerciseLap(
    startTime = Instant.parse("2022-01-02T10:10:10Z"),
    endTime = Instant.parse("2022-01-02T10:10:13Z"),
    length = 0.meters
  )
)

ExerciseSessionRecord(
  exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_CALISTHENICS,
    startTime = Instant.parse("2022-01-02T10:10:10Z"),
    endTime = Instant.parse("2022-01-02T10:10:13Z"),
  startZoneOffset = ZoneOffset.UTC,
  endZoneOffset = ZoneOffset.UTC,
  segments = segments,
  laps = laps,
  route = route,
  metadata = Metadata.manualEntry()
)