Enregistrer un exercice avec ExerciseClient

Services Santé propose une assistance de premier ordre pour les applications d'entraînement via ExerciseClient. Avec ExerciseClient, votre application peut contrôler quand un exercice est en cours, ajouter des objectifs et obtenir des informations sur l'état de l'exercice, sur les événements d'exercice ou sur d'autres métriques souhaitées. Pour en savoir plus, consultez la liste complète des types d'exercices pris en charge par Services Santé.

Consultez l'exemple d'exercice sur GitHub.

Ajouter des dépendances

Pour ajouter une dépendance à Services Santé, vous devez ajouter le dépôt Maven de Google à votre projet. Pour en savoir plus, consultez les informations sur le dépôt Maven de Google.

Ensuite, dans le fichier build.gradle, au niveau du module, ajoutez la dépendance suivante :

Groovy

dependencies {
    implementation "androidx.health:health-services-client:1.1.0-alpha02"
}

Kotlin

dependencies {
    implementation("androidx.health:health-services-client:1.1.0-alpha02")
}

Structure de l'application

Utilisez la structure d'application suivante lorsque vous créez une application d'exercice avec Services Santé :

  • Conservez vos écrans et la navigation au sein d'une activité principale.
  • Gérez l'état de l'entraînement, les données des capteurs, l'activité en cours et les données avec un service de premier plan.
  • Stockez des données avec Room et utilisez WorkManager pour importer des données.

Lors de la préparation d'un entraînement et pendant l'entraînement, votre activité peut être arrêtée pour plusieurs raisons. L'utilisateur peut passer à une autre application ou revenir au cadran. Le système peut afficher un élément par-dessus votre activité ou l'écran peut s'éteindre après une période d'inactivité. Utilisez un ForegroundService en continu avec ExerciseClient pour garantir le bon fonctionnement de l'ensemble de l'entraînement.

L'utilisation d'un ForegroundService vous permet d'utiliser l'API Ongoing Activity pour afficher un indicateur sur les surfaces de votre montre, ce qui permet à l'utilisateur de revenir rapidement à l'entraînement.

Il est essentiel que vous demandiez les données de localisation de manière appropriée dans votre service de premier plan. Dans le fichier manifeste, spécifiez foregroundServiceType="location" et les autorisations appropriées.

Utilisez AmbientLifecycleObserver pour l'activité de pré-entraînement, qui contient l'appel prepareExercise(), et pour votre activité d'entraînement. Cependant, ne mettez pas à jour l'affichage pendant l'entraînement en mode Bruit ambiant : Services Santé regroupe les données d'entraînement lorsque l'écran de l'appareil est en mode Bruit ambiant pour économiser de l'énergie. Les informations affichées peuvent donc ne pas être récentes. Pendant les entraînements, affichez des données pertinentes pour l'utilisateur, avec des informations à jour ou un écran vide.

Vérifier les fonctionnalités

Chaque ExerciseType accepte certains types de données pour les métriques et les objectifs d'exercice. Vérifiez ces fonctionnalités au démarrage, car elles peuvent varier en fonction de l'appareil. Par exemple, certains appareils ne prendront pas en charge un type d'exercice ou une fonction spécifique, comme la mise en pause automatique. De plus, les fonctionnalités d'un appareil peuvent changer au fil du temps, avec une mise à jour logicielle par exemple.

Au démarrage de l'application, interrogez les fonctionnalités de l'appareil, puis stockez et traitez les éléments suivants :

  • Les exercices pris en charge par la plate-forme
  • Les fonctionnalités prises en charge pour chaque exercice
  • Les types de données pris en charge pour chaque exercice
  • Les autorisations requises pour chacun de ces types de données

Utilisez ExerciseCapabilities.getExerciseTypeCapabilities() avec le type d'exercice souhaité pour voir le type de métriques que vous pouvez demander, les objectifs d'exercice que vous pouvez configurer et les autres fonctionnalités disponibles pour ce type. Ce processus est illustré dans l'exemple suivant :

val healthClient = HealthServices.getClient(this /*context*/)
val exerciseClient = healthClient.exerciseClient
lifecycleScope.launch {
    val capabilities = exerciseClient.getCapabilitiesAsync().await()
    if (ExerciseType.RUNNING in capabilities.supportedExerciseTypes) {
        runningCapabilities =
            capabilities.getExerciseTypeCapabilities(ExerciseType.RUNNING)
    }
}

Dans le fichier ExerciseTypeCapabilities renvoyé, supportedDataTypes répertorie les types de données pour lesquels vous pouvez demander des données. Cela varie selon l'appareil. Veillez donc à ne pas demander de DataType non compatible. Sinon, votre requête pourrait échouer.

Utilisez les champs supportedGoals et supportedMilestones pour déterminer si l'exercice peut prendre en charge un objectif d'exercice que vous souhaitez créer.

Si votre application autorise l'utilisateur à utiliser la mise en pause automatique, vous devez vérifier que cette fonctionnalité est compatible avec l'appareil à l'aide de supportsAutoPauseAndResume. ExerciseClient refusera les requêtes non compatibles avec l'appareil.

L'exemple suivant vérifie la prise en charge du type de données HEART_RATE_BPM, la fonctionnalité d'objectif STEPS_TOTAL et la fonctionnalité de mise en pause automatique :

// Whether we can request heart rate metrics.
supportsHeartRate = DataType.HEART_RATE_BPM in runningCapabilities.supportedDataTypes

// Whether we can make a one-time goal for aggregate steps.
val stepGoals = runningCapabilities.supportedGoals[DataType.STEPS_TOTAL]
supportsStepGoals = 
    (stepGoals != null && ComparisonType.GREATER_THAN_OR_EQUAL in stepGoals)

// Whether auto-pause is supported.
val supportsAutoPause = runningCapabilities.supportsAutoPauseAndResume

Demander à recevoir des informations sur l'état de l'exercice

Les mises à jour sur l'exercice sont transmises à un écouteur. Votre application ne peut enregistrer qu'un seul écouteur à la fois. Configurez votre écouteur avant de commencer l'entraînement, comme illustré dans l'exemple suivant. Votre écouteur ne reçoit que des informations sur les exercices de votre application.

val callback = object : ExerciseUpdateCallback {
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
        val exerciseStateInfo = update.exerciseStateInfo
        val activeDuration = update.activeDurationCheckpoint
        val latestMetrics = update.latestMetrics
        val latestGoals = update.latestAchievedGoals
    }

    override fun onLapSummaryReceived(lapSummary: ExerciseLapSummary) {
        // For ExerciseTypes that support laps, this is called when a lap is marked.
    }

    override fun onAvailabilityChanged(
        dataType: DataType<*, *>,
        availability: Availability
    ) {
        // Called when the availability of a particular DataType changes.
        when {
            availability is LocationAvailability -> // Relates to Location/GPS.
            availability is DataTypeAvailability -> // Relates to another DataType.
        }
    }
}
exerciseClient.setUpdateCallback(callback)

Gérer la durée de vie de l'exercice

Services Santé prend en charge au maximum un exercice à la fois pour toutes les applications de l'appareil. Si le suivi d'un exercice est en cours et qu'une autre application démarre le suivi d'un nouvel exercice, le suivi du premier est arrêté.

Avant de commencer l'exercice, procédez comme suit :

  • Vérifier si un exercice est déjà en cours de suivi et réagir en conséquence. Par exemple, l'application peut demander confirmation à l'utilisateur avant d'abandonner un exercice pour commencer à en suivre un nouveau.

L'exemple suivant montre comment vérifier un exercice existant avec getCurrentExerciseInfoAsync :

lifecycleScope.launch {
    val exerciseInfo = exerciseClient.getCurrentExerciseInfoAsync().await()
    when (exerciseInfo.exerciseTrackedStatus) {
        OTHER_APP_IN_PROGRESS -> // Warn user before continuing, will stop the existing workout.
        OWNED_EXERCISE_IN_PROGRESS -> // This app has an existing workout.
        NO_EXERCISE_IN_PROGRESS -> // Start a fresh workout.
    }
}

Autorisations

Lorsque vous utilisez ExerciseClient, assurez-vous que votre application demande et conserve les autorisations nécessaires. Si votre application utilise des données LOCATION, assurez-vous qu'elle demande et conserve également les autorisations appropriées.

Pour tous les types de données, avant d'appeler prepareExercise() ou startExercise(), procédez comme suit :

  • Spécifiez les autorisations appropriées pour les types de données demandés dans le fichier AndroidManifest.xml.
  • Vérifiez que l'utilisateur a accordé les autorisations nécessaires. Pour en savoir plus, consultez la section Exiger des autorisations d'applications. Services Santé rejette la requête si les autorisations nécessaires ne sont pas déjà accordées.

Pour les données de localisation, procédez comme suit :

Se préparer pour un entraînement

Certains capteurs, tels que le GPS ou la fréquence cardiaque, peuvent prendre un peu de temps pour être prêts, ou l'utilisateur peut vouloir consulter ses données avant de commencer son entraînement. La méthode facultative prepareExerciseAsync() permet à ces capteurs de se mettre en marche et de recevoir des données sans démarrer le minuteur de l'entraînement. La activeDuration n'est pas affectée par cette durée de préparation.

Avant d'appeler prepareExerciseAsync(), procédez comme suit :

  • Vérifiez le paramètre de localisation à l'échelle de la plate-forme. L'utilisateur contrôle ce paramètre dans le menu principal des paramètres. Ce paramètre est différent de la vérification des autorisations au niveau de l'application.

    Si le paramètre est désactivé, informez l'utilisateur qu'il a refusé l'accès à la localisation et invitez-le à l'activer si votre application a besoin de la localisation.

  • Vérifiez que votre application dispose des autorisations d'exécution pour les capteurs corporels, la reconnaissance de l'activité et la géolocalisation précise. Pour les autorisations manquantes, demandez à l'utilisateur des autorisations d'exécution fournissant un contexte approprié. Si l'utilisateur n'accorde pas d'autorisation spécifique, supprimez les types de données associés à l'autorisation de l'appel à prepareExerciseAsync(). Si aucune autorisation concernant le capteur corporel ou la localisation n'est accordée, n'appelez pas prepareExerciseAsync(), car l'appel de préparation est destiné spécifiquement à acquérir une fréquence cardiaque ou une position GPS stable avant de commencer un exercice. L'application peut toujours obtenir la distance, le rythme, la vitesse et d'autres métriques basées sur les pas qui ne nécessitent pas ces autorisations.

Procédez comme suit pour vous assurer que votre appel à prepareExerciseAsync() aboutit :

  • Utilisez AmbientLifecycleObserver pour l'activité de pré-entraînement qui contient l'appel de préparation.
  • Appelez prepareExerciseAsync() depuis votre service de premier plan. S'il n'est pas en service et qu'il est lié au cycle de vie de l'activité, la préparation du capteur risque d'être inutilement interrompue.
  • Appelez endExercise() pour désactiver les capteurs et réduire la consommation d'énergie si l'utilisateur quitte l'activité précédant l'entraînement.

L'exemple suivant montre comment appeler prepareExerciseAsync() :

val warmUpConfig = WarmUpConfig(
    ExerciseType.RUNNING,
    setOf(
        DataType.HEART_RATE_BPM,
        DataType.LOCATION
    )
)
// Only necessary to call prepareExerciseAsync if body sensor or location
//permissions are given
exerciseClient.prepareExerciseAsync(warmUpConfig).await()

// Data and availability updates are delivered to the registered listener.

Une fois que l'application est à l'état PREPARING, les informations sur la disponibilité des capteurs sont transmises entre ExerciseUpdateCallback et onAvailabilityChanged(). Ces informations peuvent ensuite être présentées à l'utilisateur pour qu'il puisse décider s'il souhaite commencer son entraînement.

Commencer l'entraînement

Lorsque vous souhaitez commencer un exercice, créez une ExerciseConfig pour configurer le type d'exercice, les types de données pour lesquels vous souhaitez recevoir des métriques, ainsi que les objectifs ou les jalons de l'exercice.

Les objectifs d'exercice sont constitués d'un DataType et d'une condition. Les objectifs d'exercice sont ponctuels et sont déclenchés lorsqu'une condition est remplie (par exemple, lorsque l'utilisateur parcourt une certaine distance). Vous pouvez également définir un jalon d'exercice, qui se déclenchera plusieurs fois, par exemple chaque fois que l'utilisateur parcourra un certain nombre de kilomètres au-delà de la distance définie.

L'exemple suivant montre comment créer un objectif de chaque type :

const val CALORIES_THRESHOLD = 250.0
const val DISTANCE_THRESHOLD = 1_000.0 // meters

suspend fun startExercise() {
    // Types for which we want to receive metrics.
    val dataTypes = setOf(
        DataType.HEART_RATE_BPM,
        DataType.CALORIES_TOTAL,
        DataType.DISTANCE
    )

    // Create a one-time goal.
    val calorieGoal = ExerciseGoal.createOneTimeGoal(
        DataTypeCondition(
            dataType = DataType.CALORIES_TOTAL,
            threshold = CALORIES_THRESHOLD,
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        )
    )

    // Create a milestone goal. To make a milestone for every kilometer, set the initial
    // threshold to 1km and the period to 1km.
    val distanceGoal = ExerciseGoal.createMilestone(
        condition = DataTypeCondition(
            dataType = DataType.DISTANCE_TOTAL,
            threshold = DISTANCE_THRESHOLD,
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        ),
        period = DISTANCE_THRESHOLD
    )

    val config = ExerciseConfig(
        exerciseType = ExerciseType.RUNNING,
        dataTypes = dataTypes,
        isAutoPauseAndResumeEnabled = false,
        isGpsEnabled = true,
        exerciseGoals = mutableListOf<ExerciseGoal<Double>>(calorieGoal, distanceGoal)
    )
    exerciseClient.startExerciseAsync(config).await()
}

Vous pouvez également compter les tours pour tous les exercices. Services Santé fournit un ExerciseLapSummary avec des métriques agrégées sur la durée des tours.

L'exemple précédent montre l'utilisation de isGpsEnabled, qui doit être défini sur true lors de la requête de données de localisation. Cependant, l'utilisation du GPS peut aussi vous aider pour d'autres métriques. Si ExerciseConfig spécifie la distance en tant que DataType, le nombre de pas est utilisé par défaut pour estimer la distance. Si vous activez le GPS, vous pouvez utiliser les informations de localisation pour estimer la distance.

Interrompre, reprendre et terminer un entraînement

Vous pouvez suspendre, reprendre et arrêter des entraînements à l'aide de la méthode appropriée, telle que pauseExerciseAsync() ou endExerciseAsync().

Utilisez l'état issu de ExerciseUpdate comme source de référence. L'entraînement n'est pas considéré comme suspendu lorsque l'appel à pauseExerciseAsync() est renvoyé, mais lorsque cet état est reflété dans le message ExerciseUpdate. ll est particulièrement important de prendre cela en compte pour les états d'interface utilisateur. Si l'utilisateur appuie sur "Pause", désactivez le bouton "Pause" et appelez pauseExerciseAsync() sur Services Santé. Attendez que l'état de Services Santé soit mis en pause via ExerciseUpdate.exerciseStateInfo.state, puis faites basculer le bouton pour qu'il permette à l'utilisateur de reprendre son entraînement. La mise à jour de l'état de Services Santé peut prendre un peu plus de temps que l'appui sur le bouton. Par conséquent, si vous associez toutes les modifications d'UI aux appuis sur le bouton, il se peut que l'UI ne soit pas synchronisée avec l'état de Services Santé.

Gardez cela à l'esprit dans les situations suivantes :

  • La mise en pause automatique est activée : l'entraînement peut être mis en pause ou démarré sans interaction de l'utilisateur.
  • Une autre application démarre un entraînement : votre entraînement peut être arrêté sans interaction de l'utilisateur.

Si l'entraînement de votre application est arrêté par une autre application, votre application doit gérer cette situation de façon optimale :

  • Enregistrez l'état d'entraînement partiel pour que la progression de l'utilisateur ne soit pas effacée.
  • Supprimez l'icône de l'activité en cours et envoyez une notification à l'utilisateur pour l'informer que son entraînement a été terminé par une autre application.

Gérez également le cas où les autorisations sont révoquées lors d'un exercice en cours. Cette information est transmise via l'état isEnded, avec AUTO_END_PERMISSION_LOST comme ExerciseEndReason. Traitez ce cas de la même manière que l'arrêt par une autre application : enregistrez l'état partiel, supprimez l'icône d'activité en cours et informez l'utilisateur.

L'exemple suivant montre comment vérifier l'arrêt de manière appropriée :

val callback = object : ExerciseUpdateCallback {
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
        if (update.exerciseStateInfo.state.isEnded) {
            // Workout has either been ended by the user, or otherwise terminated
        }
        ...
    }
    ...
}

Gérer la durée active

Pendant un exercice, une application peut afficher la durée active de l'entraînement. L'application, Services Santé et le microcontrôleur de l'appareil, ou MCU, (processeur à faible consommation d'énergie responsable du suivi des entraînements) doivent tous être synchronisés avec la même durée active. Pour faciliter cette gestion, Services Santé envoie un ActiveDurationCheckpoint qui fournit un point d'ancrage à partir duquel l'application peut lancer son minuteur.

Étant donné que la durée active est envoyée par le MCU et peut mettre un peu de temps à arriver dans l'application, ActiveDurationCheckpoint contient deux propriétés :

  • activeDuration : durée d'activité de l'exercice
  • time : date à laquelle la durée active ci-dessus a été calculée

Dans l'application, la durée active d'un exercice peut par conséquent être calculée à partir d'ActiveDurationCheckpoint à l'aide de l'équation suivante :

(now() - checkpoint.time) + checkpoint.activeDuration

Cette équation tient compte du petit delta entre la durée active calculée sur le MCU et l'arrivée dans l'application. Elle permet d'alimenter un chronomètre dans l'application et de s'assurer que le minuteur de l'application est parfaitement aligné sur l'heure dans Services Santé et le MCU.

Si l'exercice est mis en pause, l'application doit attendre que le minuteur soit relancé dans l'interface utilisateur jusqu'à ce que le temps calculé dépasse celui qui est affiché dans l'interface utilisateur. En effet, le signal de mise en pause peut atteindre Services Santé et le MCU avec un léger retard. Par exemple, si l'application est suspendue à t=10 secondes, Services Santé risque de ne pas fournir la mise à jour PAUSED à l'application avant t=10,2 secondes.

Utiliser les données d'ExerciseClient

Les métriques correspondant aux types de données enregistrés par votre application sont transmises dans des messages ExerciseUpdate.

Le processeur ne transmet les messages que lorsqu'il est activé ou lorsqu'une période de référence maximale est atteinte (par exemple, toutes les 150 secondes). Ne vous fiez pas à la fréquence de ExerciseUpdate pour faire avancer un chronomètre avec l'activeDuration. Consultez l'exemple d'exercice sur GitHub pour découvrir comment implémenter un chronomètre indépendant.

Lorsqu'un utilisateur commence un entraînement, des messages ExerciseUpdate peuvent être distribués fréquemment, par exemple toutes les secondes. Lorsque l'utilisateur commence l'entraînement, l'écran peut s'éteindre. Services Santé peuvent ensuite diffuser des données moins souvent, mais toujours échantillonnées à la même fréquence, pour éviter d'activer le processeur principal. Lorsque l'utilisateur regarde l'écran, toutes les données en cours de traitement par lot sont immédiatement transmises à votre application.

Contrôler le débit de traitement par lot

Dans certains cas, vous pouvez contrôler la fréquence à laquelle votre application reçoit certains types de données lorsque l'écran est éteint. Un objet BatchingMode permet à votre application d'ignorer le comportement de traitement par lot par défaut pour obtenir des envois de données plus fréquents.

Pour configurer le débit de traitement par lot, procédez comme suit :

  1. Vérifiez si la définition BatchingMode particulière est prise en charge par l'appareil :

    // Confirm BatchingMode support to control heart rate stream to phone.
    suspend fun supportsHrWorkoutCompanionMode(): Boolean {
        val capabilities = exerciseClient.getCapabilities()
        return BatchingMode.HEART_RATE_5_SECONDS in
                capabilities.supportedBatchingModeOverrides
    }
    
  2. Spécifiez que l'objet ExerciseConfig doit utiliser un BatchingMode particulier, comme indiqué dans l'extrait de code suivant.

    val config = ExerciseConfig(
        exerciseType = ExerciseType.WORKOUT,
        dataTypes = setOf(
            DataType.HEART_RATE_BPM,
            DataType.TOTAL_CALORIES
        ),
        // ...
        batchingModeOverrides = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    )
    
  3. Si vous le souhaitez, vous pouvez configurer BatchingMode de façon dynamique pendant l'entraînement, au lieu de conserver un comportement de traitement par lot spécifique pendant toute la durée de l'entraînement :

    val desiredModes = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    exerciseClient.overrideBatchingModesForActiveExercise(desiredModes)
    
  4. Pour effacer le BatchingMode personnalisé et revenir au comportement par défaut, transmettez un ensemble vide dans exerciseClient.overrideBatchingModesForActiveExercise().

Codes temporels

Le point dans le temps de chaque donnée correspond à la durée écoulée depuis le démarrage de l'appareil. Pour le convertir en horodatage, procédez comme suit :

val bootInstant =
    Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime())

Cette valeur peut ensuite être utilisée avec getStartInstant() ou getEndInstant() pour chaque point de données.

Précision des données

Certains types de données peuvent comporter des informations relatives à la précision associées à chaque point de données. Ceci est représenté dans la propriété accuracy.

Les classes HrAccuracy et LocationAccuracy peuvent être renseignées respectivement pour les types de données HEART_RATE_BPM et LOCATION. Le cas échéant, utilisez la propriété accuracy pour déterminer si chaque point de données est suffisamment précis pour votre application.

Stocker et importer des données

Utilisez Room pour conserver les données fournies par Services Santé. L'importation des données a lieu à la fin de l'exercice, à l'aide d'un mécanisme comme Work Manager. Cela garantit que les appels réseau pour importer des données sont différés jusqu'à la fin de l'exercice, ce qui réduit la consommation d'énergie pendant l'exercice et simplifie le travail.

Liste de contrôle pour l'intégration

Avant de publier votre application qui utilise l'ExerciseClient des Services Santé, consultez la checklist suivante pour vous assurer que votre expérience utilisateur évite certains problèmes courants. Vérifiez les points suivants :

  • Votre application vérifie les fonctionnalités du type d'exercice et celles de l'appareil à chaque exécution. De cette façon, vous pouvez détecter lorsqu'un appareil ou un exercice en particulier n'est pas compatible avec l'un des types de données dont votre application a besoin.
  • Vous demandez et gérez les autorisations nécessaires, puis vous les spécifiez dans votre fichier manifeste. Avant d'appeler prepareExerciseAsync(), votre application confirme que les autorisations d'exécution sont accordées.
  • Votre application utilise getCurrentExerciseInfoAsync() pour gérer les cas où :
    • Un exercice est déjà en cours de suivi et votre application remplace l'exercice précédent.
    • Une autre application a arrêté votre exercice. Cela peut se produire lorsque l'utilisateur rouvre l'application, il reçoit un message lui expliquant que l'exercice s'est arrêté parce qu'une autre application a pris le relais.
  • Si vous utilisez des données LOCATION :
    • Votre application conserve un ForegroundService avec le foregroundServiceType correspondant pendant toute la durée de l'exercice (y compris l'appel de préparation).
    • Vérifiez que le GPS est activé sur l'appareil à l'aide de isProviderEnabled(LocationManager.GPS_PROVIDER) et invite l'utilisateur à ouvrir les paramètres de localisation si nécessaire.
    • Pour les cas d'utilisation exigeants, où la réception de données de localisation avec une faible latence est d'une grande importance, envisagez d'intégrer le Fused Location Provider (FLP) et d'utiliser ses données comme correction d'emplacement initiale. Lorsque les Services Santé fournissent des informations de localisation plus stables, utilisez-les à la place de FLP.
  • Si votre application nécessite l'importation de données, les appels réseau pour importer des données sont différés jusqu'à la fin de l'exercice. Sinon, tout au long de l'exercice, votre application effectue les appels réseau nécessaires avec parcimonie.