Cómo registrar un ejercicio con ExerciseClient

Los Servicios de salud proporcionan asistencia de primera clase para las apps de entrenamiento a través de ExerciseClient. Con ExerciseClient, tu app puede controlar un ejercicio en curso, agregar objetivos de entrenamiento y obtener actualizaciones sobre el estado del ejercicio, los eventos de ejercicio y otras métricas deseadas. Para obtener más información, consulta la lista completa de tipos de ejercicio que admiten los Servicios de salud.

Consulta la muestra de ejercicio en GitHub.

Agrega dependencias

Para agregar una dependencia en los Servicios de salud, debes agregar el repositorio de Maven de Google a tu proyecto. Para obtener información relacionada, consulta el repositorio de Maven de Google.

Luego, en el archivo build.gradle a nivel del módulo, agrega la siguiente dependencia:

Groovy

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

Kotlin

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

Estructura de app

Usa la siguiente estructura de app cuando compiles una app de ejercicios con los Servicios de salud:

Cuando te preparas para un entrenamiento y durante el entrenamiento, es posible que la actividad se detenga por varios motivos. El usuario podría cambiar a otra app o volver a la cara de reloj. Es posible que el sistema muestre algún elemento sobre tu actividad o que se apague la pantalla después de un período de inactividad. Usa un ForegroundService que se ejecute de forma continua junto con ExerciseClient para garantizar una operación correcta para todo el entrenamiento.

Usar un objeto ForegroundService te permite usar la API de Ongoing Activity para mostrar un indicador en las plataformas de tu reloj, lo que le permite al usuario volver rápidamente al entrenamiento.

Es fundamental que solicites los datos de ubicación de manera adecuada en tu servicio en primer plano. En tu archivo de manifiesto, especifica foregroundServiceType="location" y los permisos adecuados.

Usa AmbientLifecycleObserver para tu actividad previa al entrenamiento, que contiene la llamada de prepareExercise(), y para tu actividad de entrenamiento. Sin embargo, no debes actualizar la pantalla durante el entrenamiento en el modo ambiente, ya que los Servicios de salud agrupan por lotes los datos de entrenamiento cuando la pantalla del dispositivo está en modo ambiente para ahorrar energía, por lo que es posible que la información que se muestra no sea reciente. Durante los entrenamientos, muestra datos que tengan sentido para el usuario, como información actualizada o una pantalla en blanco.

Verifica las funciones

Cada ExerciseType admite ciertos tipos de datos para las métricas y los objetivos de ejercicio. Verifica estas capacidades al inicio, ya que pueden variar según el dispositivo. Es posible que un dispositivo no admita un tipo de ejercicio determinado o que no admita una función específica, como la pausa automática. Además, las funciones de un dispositivo pueden cambiar con el tiempo, por ejemplo, después de una actualización de software.

Cuando se inicie la app, consulta las capacidades del dispositivo y almacena y procesa lo siguiente:

  • Los ejercicios que admite la plataforma
  • Las funciones que se admiten en cada ejercicio
  • Los tipos de datos que se admiten para cada ejercicio
  • Los permisos necesarios para cada uno de esos tipos de datos

Usa ExerciseCapabilities.getExerciseTypeCapabilities() con el tipo de ejercicio que desees para ver la clase de métricas que puedes solicitar, los objetivos de ejercicio que puedes configurar y otras funciones disponibles para esas métricas. Esto se muestra en el siguiente ejemplo:

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)
    }
}

Dentro de las ExerciseTypeCapabilities que se muestran, el elemento supportedDataTypes enumera los tipos de datos para los que puedes solicitar datos. Esto varía según el dispositivo, así que ten cuidado de no solicitar un DataType que no sea compatible o tu solicitud podría fallar.

Usa los campos supportedGoals y supportedMilestones para determinar si el ejercicio puede admitir el objetivo de entrenamiento que quieras crear.

Si tu app permite que el usuario utilice la pausa automática, debes verificar que esta función sea compatible con el dispositivo usando supportsAutoPauseAndResume. ExerciseClient rechaza las solicitudes que el dispositivo no admite.

En el siguiente ejemplo, se verifica la compatibilidad con el tipo de datos HEART_RATE_BPM, la capacidad de objetivo STEPS_TOTAL y la funcionalidad de pausa automática:

// 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

Regístrate para recibir actualizaciones del estado de ejercicio

Las actualizaciones del ejercicio se entregan a un objeto de escucha. Tu app solo puede registrar un objeto de escucha a la vez. Configura tu objeto de escucha antes de comenzar el entrenamiento, como se muestra en el siguiente ejemplo. Tu objeto de escucha solo recibe actualizaciones sobre los ejercicios que pertenecen a tu app.

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)

Administra la duración del ejercicio

Los Servicios de salud admiten como máximo un ejercicio a la vez en todas las apps del dispositivo. Si se realiza un seguimiento de un ejercicio y una app diferente comienza a registrar uno nuevo, finaliza el primero.

Antes de comenzar el ejercicio, haz lo siguiente:

  • Verifica si ya se está realizando el seguimiento de otro ejercicio y reacciona según corresponda. Por ejemplo, solicita al usuario la confirmación antes de anular un ejercicio anterior y comenzar a realizar un seguimiento de uno nuevo.

En el siguiente ejemplo, se muestra cómo verificar un ejercicio existente con 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.
    }
}

Permisos

Cuando uses ExerciseClient, asegúrate de que tu app solicite y mantenga los permisos necesarios. Si tu app usa datos de LOCATION, asegúrate de que la app solicite y mantenga los permisos adecuados para eso también.

Para todos los tipos de datos, antes de llamar a prepareExercise() o startExercise(), haz lo siguiente:

  • Especifica los permisos adecuados para los tipos de datos solicitados en tu archivo AndroidManifest.xml.
  • Verifica que el usuario haya otorgado los permisos necesarios. Para obtener más información, consulta Cómo solicitar permisos de la app. Los Servicios de salud rechazan la solicitud si aún no se otorgaron los permisos necesarios.

Para los datos de ubicación, sigue estos pasos adicionales:

Prepárate para un entrenamiento

Algunos sensores, como el GPS o la frecuencia cardíaca, pueden tardar un poco en entrar en calor o el usuario podría querer ver sus datos antes de comenzar su entrenamiento. El método opcional prepareExerciseAsync() permite que estos sensores se preparen y reciban datos antes de iniciar el temporizador del entrenamiento. El activeDuration no se ve afectado por este tiempo de preparación.

Antes de realizar la llamada a prepareExerciseAsync(), verifica lo siguiente:

  • Verifica la configuración de ubicación en toda la plataforma. El usuario controla este parámetro de configuración desde el menú principal de Configuración, y no es lo mismo que la verificación de permisos a nivel de la app.

    Si la configuración está desactivada, notifica al usuario que rechazó el acceso a la ubicación y pídele que la habilite si tu app requiere la ubicación.

  • Confirma que tu app tenga permisos de tiempo de ejecución para los sensores corporales, el reconocimiento de actividad y la ubicación precisa. En el caso de los permisos que faltan, solicita al usuario los permisos de tiempo de ejecución siempre que sea adecuado para el contexto. Si el usuario no otorga un permiso específico, quita los tipos de datos asociados con ese permiso de la llamada a prepareExerciseAsync(). Si no se otorgan permisos de ubicación ni del sensor corporal, no llames a prepareExerciseAsync(), ya que la llamada de preparación se realiza específicamente para adquirir una frecuencia cardíaca estable o una corrección de GPS antes de iniciar un ejercicio. De todos modos, la app puede obtener distancias según los pasos, el ritmo, la velocidad y otras métricas que no requieren esos permisos.

Para asegurarte de que la llamada a prepareExerciseAsync() se realice correctamente, haz lo siguiente:

  • Usa AmbientLifecycleObserver para la actividad de entrada en calor que incluye la llamada de preparación.
  • Llama a prepareExerciseAsync() desde tu servicio en primer plano. Si no está en un servicio y está vinculado al ciclo de vida de la actividad, la preparación del sensor puede finalizarse de forma innecesaria.
  • Llama a endExercise() para apagar los sensores y reducir el uso de energía si el usuario sale de la actividad de entrada en calor.

En el siguiente ejemplo, se muestra cómo llamar a 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.

Una vez que la app se encuentra en el estado PREPARING, las actualizaciones de disponibilidad del sensor se entregan en ExerciseUpdateCallback mediante onAvailabilityChanged(). Se puede presentar esta información al usuario para que decida si desea iniciar el entrenamiento.

Comienza el entrenamiento

Cuando desees comenzar un ejercicio, crea un ExerciseConfig a fin de configurar el tipo de ejercicio, los tipos de datos para los que deseas recibir métricas y los objetivos o logros de ejercicio.

Los objetivos de entrenamiento consisten en un DataType y una condición. Los objetivos de ejercicio son una meta única que se activa cuando se cumple una condición, por ejemplo, que el usuario corra una cierta distancia. También se puede establecer un hito de ejercicio. Los hitos del ejercicio se pueden activar varias veces, por ejemplo, cada vez que el usuario corra una distancia determinada más allá de la establecida.

En el siguiente ejemplo, se muestra cómo crear un objetivo de cada tipo:

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()
}

También puedes marcar vueltas para todos los ejercicios. Los Servicios de salud proporcionan un ExerciseLapSummary con métricas agregadas durante el período de vuelta.

En el ejemplo anterior, se muestra el uso de isGpsEnabled, que debe ser verdadero cuando se solicitan datos de ubicación. Sin embargo, el uso del GPS también puede ayudar con otras métricas. Si ExerciseConfig especifica la distancia como DataType, la configuración predeterminada será el uso de pasos para calcular la distancia. De manera opcional, si se habilita el GPS, se podrá usar la información de ubicación para calcular la distancia.

Pausa, reanuda o finaliza un entrenamiento

Puedes pausar, reanudar y finalizar entrenamientos con el método apropiado, como pauseExerciseAsync() o endExerciseAsync().

Usa el estado de ExerciseUpdate como fuente de confianza. El entrenamiento no se considera en pausa cuando se muestra la llamada a pauseExerciseAsync(), sino cuando ese estado se refleja en el mensaje ExerciseUpdate. Es especialmente importante tener esto en cuenta cuando se trata de estados de la IU. Si el usuario presiona Pausar, se inhabilita el botón de pausa y se llama a pauseExerciseAsync() en los Servicios de salud. Espera a que los Servicios de salud alcancen el estado de pausa usando ExerciseUpdate.exerciseStateInfo.state y, luego, cambia el botón para reanudar. Esto se debe a que las actualizaciones de estado de los Servicios de salud pueden tardar más en entregarse que la acción de presionar el botón, por lo que si vinculas todos los cambios de la IU a las presiones de botones, la IU puede desincronizarse con el estado de los Servicios de salud.

Ten esto en cuenta en las siguientes situaciones:

  • La pausa automática está habilitada: El entrenamiento puede pausarse o iniciarse sin interacción del usuario.
  • Otra app inicia un entrenamiento: Es posible que se finalice tu entrenamiento sin interacción del usuario.

Si otra app finaliza el entrenamiento de tu app, esta deberá controlar correctamente la finalización:

  • Guardar el estado de entrenamiento parcial para que no se borre el progreso del usuario
  • Quitar el ícono de actividad en curso y enviarle una notificación al usuario para informarle que otra app finalizó su entrenamiento

Además, debe controlarse el caso en el que se revocan los permisos durante un ejercicio en curso. Esto se envía mediante el estado isEnded, con un ExerciseEndReason de AUTO_END_PERMISSION_LOST. Controla este caso de manera similar al caso de finalización. Es decir, guarda el estado parcial, quita el ícono de actividad en curso y envía una notificación al usuario sobre lo que sucedió.

En el siguiente ejemplo, se muestra cómo verificar la finalización de forma correcta:

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
        }
        ...
    }
    ...
}

Administra la duración activa

Durante un ejercicio, una app puede mostrar la duración activa del entrenamiento. La app, los Servicios de salud y el microcontrolador Unity (MCU) del dispositivo (el procesador de bajo consumo responsable del seguimiento de ejercicios) deben sincronizarse con la misma duración activa actual. Para ayudar a administrar esto, los Servicios de salud envían un ActiveDurationCheckpoint que proporciona un punto de anclaje desde el cual la app puede iniciar su temporizador.

Debido a que la duración activa se envía desde el MCU y puede tardar un poco en llegar a la app, ActiveDurationCheckpoint contiene dos propiedades:

  • activeDuration: Cuánto tiempo estuvo activo el ejercicio
  • time: Cuándo se calculó la duración activa anterior

Por lo tanto, en la app, la duración activa de un ejercicio se puede calcular a partir de ActiveDurationCheckpoint con la siguiente ecuación:

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

Esto explica el pequeño delta entre la duración activa que se calcula en el MCU y el momento en que llega a la app. Se puede usar para propagar un cronómetro en la app y ayudar a garantizar que el temporizador de la app esté perfectamente alineado con la hora en los Servicios de salud y el MCU.

Si el ejercicio está en pausa, la app espera para reiniciar el temporizador en la IU hasta que el tiempo calculado haya superado lo que la IU muestra actualmente. Esto se debe a que la señal de pausa llega a los Servicios de salud y al MCU con una leve demora. Por ejemplo, si la app está pausada en t=10 segundos, es posible que los Servicios de salud no entreguen la actualización de PAUSED a la app hasta t=10.2 segundos.

Cómo trabajar con datos de ExerciseClient

Las métricas de los tipos de datos para los que se registró la app se entregan en mensajes ExerciseUpdate.

El procesador envía mensajes solo cuando está activo o cuando se alcanza un período máximo del informe, como cada 150 segundos. No confíes en la frecuencia de ExerciseUpdate para avanzar un cronómetro con activeDuration. Consulta la muestra de ejercicio en GitHub a fin de ver un ejemplo para implementar un cronómetro independiente.

Cuando un usuario inicia un entrenamiento, los mensajes de ExerciseUpdate pueden entregarse con frecuencia, por ejemplo, en cada segundo. Cuando el usuario inicia el entrenamiento, la pantalla podría apagarse. De esta forma, los Servicios de salud pueden tomar muestras de datos con la misma frecuencia pero no entregarlos tan seguido y evitan activar el procesador principal. Cuando el usuario mira la pantalla, cualquier dato que esté en el proceso de entrega en lotes se envía de inmediato a tu app.

Cómo controlar la tasa de lote

En algunas situaciones, es posible que quieras controlar la frecuencia con la que tu app recibe ciertos tipos de datos mientras la pantalla está apagada. Un objeto BatchingMode permite que tu app anule el comportamiento por lotes predeterminado para obtener entregas de datos con mayor frecuencia.

Para configurar la tasa de lote, completa los siguientes pasos:

  1. Verifica si el dispositivo admite la definición del BatchingMode particular:

    // 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. Especifica que el objeto ExerciseConfig debe usar un BatchingMode particular, como se muestra en el siguiente fragmento de código.

    val config = ExerciseConfig(
        exerciseType = ExerciseType.WORKOUT,
        dataTypes = setOf(
            DataType.HEART_RATE_BPM,
            DataType.TOTAL_CALORIES
        ),
        // ...
        batchingModeOverrides = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    )
    
  3. De manera opcional, puedes configurar BatchingMode de forma dinámica durante el entrenamiento, en lugar de que el comportamiento por lotes específico persista durante el entrenamiento:

    val desiredModes = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    exerciseClient.overrideBatchingModesForActiveExercise(desiredModes)
    
  4. Para borrar el BatchingMode personalizado y volver al comportamiento predeterminado, pasa un conjunto vacío a exerciseClient.overrideBatchingModesForActiveExercise().

Marcas de tiempo

El momento determinado de cada dato representa la duración desde el momento en que se inició el dispositivo. Para convertir esto en una marca de tiempo, haz lo siguiente:

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

Este valor se puede usar con getStartInstant() o getEndInstant() para cada dato.

Exactitud de los datos

Algunos tipos de datos pueden tener información de la exactitud asociada a cada dato. Esto se representa en la propiedad accuracy.

Las clases HrAccuracy y LocationAccuracy pueden propagarse para los tipos de datos HEART_RATE_BPM y LOCATION, respectivamente. Cuando esté presente, usa la propiedad accuracy para determinar si cada dato es lo suficientemente preciso para tu aplicación.

Almacena y sube datos

Usa Room para conservar los datos entregados desde los Servicios de salud. Los datos se suben al final del ejercicio mediante un mecanismo como Work Manager. De esta forma se garantiza que las llamadas de red para subir datos se aplacen hasta que el ejercicio finalice, lo que minimiza el consumo de energía durante el ejercicio y simplifica el trabajo.

Lista de verificación de integración

Antes de publicar tu app que usa ExerciseClient de los Servicios de salud, consulta la siguiente lista de tareas para asegurarte de que la experiencia del usuario evite algunos problemas habituales. Confirma lo siguiente:

  • Tu app verifica las capacidades del tipo de ejercicio y las capacidades del dispositivo cada vez que se ejecuta. De esa manera, puedes detectar cuándo un dispositivo o ejercicio en particular no admite uno de los tipos de datos que necesita tu app.
  • Solicitas y mantienes los permisos necesarios y los especificas en tu archivo de manifiesto. Antes de llamar a prepareExerciseAsync(), tu app confirma que se otorgaron los permisos de tiempo de ejecución.
  • Tu app usa getCurrentExerciseInfoAsync() para controlar los casos en los que:
    • Ya se está realizando el seguimiento de un ejercicio, y tu app anula el anterior.
    • Otra app finalizó tu ejercicio. Esto podría ocurrir cuando el usuario vuelve a abrir la app y recibe un mensaje que explica que el ejercicio se detuvo porque otra app lo reemplazó.
  • Si usas datos de LOCATION, haz lo siguiente:
    • Tu app mantiene un ForegroundService con el foregroundServiceType correspondiente durante el ejercicio (incluida la llamada de preparación).
    • Verifica que el GPS esté habilitado en el dispositivo con isProviderEnabled(LocationManager.GPS_PROVIDER) y le solicita al usuario que abra la configuración de la ubicación si es necesario.
    • Para casos de uso exigentes, en los que recibir datos de ubicación con baja latencia es de gran importancia, considera integrar el proveedor de ubicación combinada (FLP) y usar sus datos como una corrección de ubicación inicial. Cuando haya información de ubicación más estable disponible de los Servicios de salud, úsala en lugar de FLP.
  • Si tu app requiere una carga de datos, las llamadas de red para subir datos se pospondrán hasta que el ejercicio finalice. De lo contrario, durante el ejercicio, tu app realizará las llamadas de red necesarias con moderación.