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 Ejemplo de ejercicio en GitHub.
Cómo agregar 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-alpha03" }
Kotlin
dependencies { implementation("androidx.health:health-services-client:1.1.0-alpha03") }
Estructura de app
Usa la siguiente estructura de app cuando compiles una app de ejercicios con los Servicios de salud:
- Mantén las pantallas y la navegación dentro de una actividad principal.
- Administra el estado del entrenamiento, los datos del sensor, la actividad en curso y los datos con un servicio en primer plano.
- Almacena datos con Room y usa WorkManager para subirlos.
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 el servicio en primer plano necesario tipos y permisos:
<manifest ...> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application ...> <!-- If your app is designed only for devices that run Wear OS 4 or lower, use android:foregroundServiceType="location" instead. --> <service android:name=".MyExerciseSessionRecorder" android:foregroundServiceType="health|location"> </service> </application> </manifest>
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.
Cómo verificar 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 el
permisos necesarios.
Si tu app usa datos de LOCATION
, asegúrate de que solicite y mantenga las
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:
- Verifica que el GPS esté habilitado en el dispositivo con
isProviderEnabled(LocationManager.GPS_PROVIDER)
. Solicítale al usuario que abra la configuración de ubicación. si es necesario. - Asegúrate de mantener un
ForegroundService
con elforegroundServiceType
adecuado durante el entrenamiento.
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 aprepareExerciseAsync()
, 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 la unidad de microcontrolador del dispositivo
(MCU), el sistema de administración
encargado del seguimiento del ejercicio; todos deben estar sincronizados,
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 ejerciciotime
: 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
Ejemplo de ejercicio
en GitHub para ver un ejemplo de cómo 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:
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 }
Especifica que el objeto
ExerciseConfig
debe usar unBatchingMode
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) )
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)
Para borrar el
BatchingMode
personalizado y volver al comportamiento predeterminado, pasa un conjunto vacío aexerciseClient.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 los Servicios de salud ExerciseClient
, consulta
la siguiente lista de tareas para garantizar 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 la app. De esta manera, puedes detectar cuando un dispositivo o ejercicio en particular no admita 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 hayan otorgado los permisos de tiempo de ejecución. - Tu app usa
getCurrentExerciseInfoAsync()
para controlar los casos en los que:- Ya se está registrando un ejercicio y tu app anula el anterior ejercicio.
- Otra app finalizó el ejercicio. Esto puede ocurrir cuando el usuario vuelve a abrir la app, recibe un mensaje que explica que el ejercicio se detuvo porque otra app tomó el control.
- Si usas datos de
LOCATION
, haz lo siguiente:- Tu app mantiene un
ForegroundService
con el correspondienteforegroundServiceType
durante todo el ejercicio (incluidos la llamada de preparación). - Comprueba que el GPS esté habilitado en el dispositivo con
isProviderEnabled(LocationManager.GPS_PROVIDER)
y le solicita al usuario que abre la configuración de ubicación si es necesario. - Para casos de uso exigentes, en los que la recepción de datos de ubicación con baja la latencia es muy importante, considera integrar el módulo Proveedor de ubicación (FLP) y usar sus datos como un ajuste de ubicación inicial. Cuando es más estable la información de ubicación está disponible en los Servicios de salud, úsala de FLP.
- Tu app mantiene un
- Si tu app requiere la carga de datos, todas las llamadas de red para subir datos se se pospone hasta que el ejercicio haya finalizado. De lo contrario, durante el ejercicio, tu la app realiza las llamadas de red necesarias con moderación.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado.
- Actualizaciones de datos pasivos
- Servicios de salud en Wear OS
- Cómo comenzar a usar tarjetas