Сервисы наблюдения за здоровьем поддерживают приложения для тренировок через ExerciseClient.
С помощью ExerciseClient ваше приложение может контролировать, когда выполняется тренировка, добавлять цели тренировки и получать обновления о состоянии тренировки, событиях тренировки или других показателях. Подробную информацию можно найти в полном списке типов тренировок, поддерживаемых сервисами наблюдения за здоровьем.
См. пример упражнения на GitHub.
Добавьте зависимости
Чтобы добавить зависимость от сервисов наблюдения за здоровьем, добавьте в проект репозиторий Google Maven. Дополнительную информацию можно найти в статье Репозиторий Maven от Google.
Затем в файл build.gradle на уровне модуля добавьте следующую зависимость:
классный
dependencies { implementation "androidx.health:health-services-client:1.1.0" }
Котлин
dependencies { implementation("androidx.health:health-services-client:1.1.0") }
Структура приложения
При создании приложения для тренировок с использованием сервисов наблюдения за здоровьем используйте следующую структуру:
- Размещайте экраны и навигацию в основном действии.
- Управляйте состоянием тренировки, данными датчиков, текущей активностью и данными с помощью активной службы.
- Храните данные в Room и загружайте их с помощью WorkManager.
При подготовке к тренировке и во время нее активность может быть остановлена по разным причинам. Пользователь может переключиться на другое приложение или вернуться к циферблату. Система может показывать что-то поверх вашего действия, или экран может выключаться после периода бездействия.
Используйте непрерывно работающий ForegroundService в сочетании с ExerciseClient, чтобы обеспечить правильную работу на протяжении всей тренировки.
Используя ForegroundService, вы можете задействовать Ongoing Activity API, чтобы показывать индикатор на циферблате. Это позволит пользователю быстро вернуться к тренировке.
Важно, чтобы активная служба запрашивала данные о местоположении надлежащим образом. В файле манифеста укажите необходимые типы активных служб и разрешения.
<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>
Используйте AmbientLifecycleObserver для действий перед тренировкой, которые содержат вызов prepareExercise(), и для самой тренировки. Однако не обновляйте экран во время тренировки в спящем режиме. Это связано с тем, что сервисы наблюдения за здоровьем собирают данные о тренировке, когда экран устройства находится в спящем режиме, чтобы экономить заряд батареи, поэтому отображаемая информация может быть неактуальной. Во время тренировок показывайте пользователю актуальные данные или пустой экран.
Проверить возможности
Каждый ExerciseType поддерживает определенные типы данных для показателей и целей тренировок. Проверяйте эти возможности при запуске, так как они могут различаться в зависимости от устройства. Устройство может не поддерживать определенный тип тренировки или определенную функцию, например автопаузу. Кроме того, возможности устройства могут меняться со временем, например после обновления программного обеспечения.
При запуске приложения запросите возможности устройства, а затем сохраните и обработайте следующие данные:
- Упражнения, которые поддерживает платформа.
- Функции, поддерживаемые для каждого упражнения.
- Типы данных, поддерживаемые для каждого упражнения.
- Разрешения, необходимые для каждого из этих типов данных.
Используйте ExerciseCapabilities.getExerciseTypeCapabilities() с выбранным типом тренировки, чтобы узнать, какие показатели можно запрашивать, какие цели тренировки можно настраивать и какие другие функции доступны для этого типа. Это показано в следующем примере:
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)
}
}
В возвращенном объекте ExerciseTypeCapabilities в поле supportedDataTypes перечислены типы данных, которые вы можете запросить. Это зависит от устройства, поэтому будьте внимательны и не запрашивайте неподдерживаемый параметр DataType, иначе запрос может не выполниться.
Используйте поля supportedGoals и supportedMilestones, чтобы определить, можно ли использовать упражнение для достижения цели, которую вы хотите создать.
Если в вашем приложении есть функция автопаузы, необходимо проверить, поддерживается ли она на устройстве, с помощью supportsAutoPauseAndResume.
ExerciseClient отклоняет запросы, которые не поддерживаются на устройстве.
В следующем примере проверяется поддержка типа данных HEART_RATE_BPM, возможности цели STEPS_TOTAL и функции автоматической паузы:
// 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
Регистрация для получения обновлений о статусе тренировки
Обновления тренировок доставляются слушателю. Приложение может зарегистрировать только один прослушиватель за раз. Настройте прослушиватель перед началом тренировки, как показано в примере ниже. Слушатель получает обновления только о тренировках, принадлежащих вашему приложению.
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)
Управление сроком действия упражнения
Сервисы наблюдения за здоровьем поддерживают не более одной тренировки одновременно во всех приложениях на устройстве. Если в одном приложении отслеживается тренировка, а в другом запускается отслеживание новой, первая тренировка завершается.
Перед началом тренировки выполните следующие действия:
- Проверьте, отслеживается ли уже тренировка, и отреагируйте соответствующим образом. Например, запрашивайте подтверждение, прежде чем перезаписать предыдущую тренировку и начать отслеживать новую.
В следующем примере показано, как проверить наличие существующего упражнения с помощью 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.
}
}
Разрешения
При использовании ExerciseClient убедитесь, что ваше приложение запрашивает и поддерживает необходимые разрешения.
Если ваше приложение использует данные LOCATION, убедитесь, что оно запрашивает и поддерживает соответствующие разрешения.
Для всех типов данных перед вызовом prepareExercise() или startExercise() выполните следующие действия:
- Укажите нужные разрешения для запрашиваемых типов данных в файле
AndroidManifest.xml. - Убедитесь, что пользователь предоставил необходимые разрешения. Подробнее о том, как запрашивать разрешения для приложения… Если необходимые разрешения не предоставлены, сервисы наблюдения за здоровьем отклонят запрос.
Для данных о местоположении выполните следующие дополнительные действия:
- Проверьте, включен ли GPS на устройстве, с помощью
isProviderEnabled(LocationManager.GPS_PROVIDER). При необходимости предложите пользователю открыть настройки геолокации. - Убедитесь, что
ForegroundServiceс соответствующимforegroundServiceTypeподдерживается на протяжении всей тренировки.
Подготовка к тренировке
Некоторым датчикам, например GPS или пульсометру, может потребоваться некоторое время для прогрева, или пользователь может захотеть увидеть свои данные перед началом тренировки. Необязательный метод prepareExerciseAsync() позволяет датчикам подготовиться к работе и начать получать данные до запуска таймера тренировки. Время подготовки не влияет на activeDuration.
Прежде чем звонить по номеру prepareExerciseAsync(), проверьте следующее:
Проверьте настройки геолокации на уровне платформы. Пользователь управляет этим параметром в главном меню настроек. Он отличается от проверки разрешений на уровне приложения.
Если доступ запрещен, сообщите об этом пользователю и предложите разрешить доступ, если он необходим для работы приложения.
Убедитесь, что у приложения есть динамические разрешения на доступ к нательным датчикам (уровень API 35 или ниже) или пульсу (уровень API 36 или выше), распознаванию действий и точному местоположению. Если разрешения не предоставлены, запросите динамические разрешения при запуске приложения, предоставив пользователю необходимый контекст. Если пользователь не предоставил определенное разрешение, удалите типы данных, связанные с этим разрешением, из вызова
prepareExerciseAsync(). Если у приложения нет доступа к нательным датчикам (пульсу на уровне API 36 и выше) или разрешению на доступ к данным о местоположении, не вызывайтеprepareExerciseAsync(), так как этот метод предназначен для получения стабильных данных о пульсе или местоположении по GPS перед началом тренировки. Приложение по-прежнему может получать данные о расстоянии, темпе, скорости и других показателях, основанных на количестве шагов, для которых эти разрешения не требуются.
Чтобы убедиться, что вызов prepareExerciseAsync() может быть выполнен, сделайте следующее:
- Используйте
AmbientLifecycleObserverдля действия перед тренировкой, которое содержит вызов prepare. - Вызовите
prepareExerciseAsync()из активной службы. Если он не находится в сервисе и привязан к жизненному циклу объекта activity, то подготовка датчика может быть прервана без необходимости. - Вызовите
endExercise(), чтобы отключить датчики и снизить энергопотребление, если пользователь уходит с экрана подготовки к тренировке.
Пример вызова prepareExerciseAsync():
val warmUpConfig = WarmUpConfig(
ExerciseType.RUNNING,
setOf(
DataType.HEART_RATE_BPM,
DataType.LOCATION
)
)
// Only necessary to call prepareExerciseAsync if body sensor (API level 35
// or lower), heart rate (API level 36+), or location permissions are given.
exerciseClient.prepareExerciseAsync(warmUpConfig).await()
// Data and availability updates are delivered to the registered listener.
Когда приложение находится в состоянии PREPARING, обновления доступности датчиков доставляются в ExerciseUpdateCallback через onAvailabilityChanged().
Затем эта информация может быть представлена пользователю, чтобы он мог решить, начинать ли тренировку.
Начать тренировку
Чтобы начать тренировку, создайте объект ExerciseConfig, в котором укажите тип тренировки, типы данных, для которых вы хотите получать показатели, а также цели или этапы тренировки.
Цели тренировок состоят из DataType и условия. Цели тренировок – это одноразовые цели, которые достигаются при выполнении условия, например когда пользователь пробегает определенное расстояние. Также можно установить цель по тренировкам. Этапы тренировки могут запускаться несколько раз, например каждый раз, когда пользователь пробегает определенное расстояние.
В примере ниже показано, как создать по одной цели каждого типа.
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()
}
Вы также можете отмечать круги для всех упражнений. Сервисы наблюдения за здоровьем предоставляют объект ExerciseLapSummary с показателями, агрегированными за период круга.
В предыдущем примере показано использование isGpsEnabled, которое должно быть истинным при запросе данных о местоположении. Однако использование GPS также может помочь с другими
показателями. Если в ExerciseConfig расстояние указано как DataType, по умолчанию для оценки расстояния используются шаги. Если вы включите GPS, для расчета расстояния будут использоваться данные о местоположении.
Как приостановить, возобновить и завершить тренировку
Вы можете приостанавливать, возобновлять и завершать тренировки, используя подходящий метод, например pauseExerciseAsync() или endExerciseAsync().
Используйте статус из ExerciseUpdate в качестве источника достоверной информации. Тренировка не считается приостановленной, когда возвращается вызов pauseExerciseAsync(). Это происходит, когда состояние отражается в сообщении ExerciseUpdate. Это особенно важно учитывать, когда речь идет о состояниях пользовательского интерфейса. Если пользователь нажимает кнопку паузы, отключите ее и вызовите pauseExerciseAsync() в сервисах наблюдения за здоровьем. Дождитесь, пока сервисы наблюдения за здоровьем перейдут в состояние паузы, используя ExerciseUpdate.exerciseStateInfo.state, а затем нажмите кнопку, чтобы возобновить работу. Это связано с тем, что обновление состояния сервисов наблюдения за здоровьем может занять больше времени, чем нажатие кнопки. Если вы привяжете все изменения пользовательского интерфейса к нажатиям кнопок, пользовательский интерфейс может рассинхронизироваться с состоянием сервисов наблюдения за здоровьем.
Учитывайте это в следующих ситуациях:
- Автопауза включена. Тренировка может приостанавливаться или запускаться без участия пользователя.
- Другое приложение начинает тренировку. Ваша тренировка может быть завершена без участия пользователя.
Если тренировка в вашем приложении прервана другим приложением, ваше приложение должно корректно обработать это событие:
- Сохраняйте состояние тренировки, чтобы прогресс пользователя не был утерян.
- Удалите значок текущей активности и отправьте пользователю уведомление о том, что тренировка была завершена другим приложением.
Также обработайте случай, когда разрешения отзываются во время тренировки. Оно отправляется с использованием состояния isEnded и ExerciseEndReason AUTO_END_PERMISSION_LOST. Обработайте этот случай так же, как и завершение: сохраните частичное состояние, удалите значок текущей активности и отправьте пользователю уведомление о том, что произошло.
В примере ниже показано, как правильно проверить, завершился ли процесс.
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
}
...
}
...
}
Управление продолжительностью активности
Во время тренировки приложение может показывать ее продолжительность. Приложение, сервисы наблюдения за здоровьем и микроконтроллер устройства (MCU) – процессор с низким энергопотреблением, отвечающий за отслеживание тренировок, – должны быть синхронизированы и иметь одинаковую текущую активную продолжительность. Чтобы помочь в этом, сервисы наблюдения за здоровьем отправляют ActiveDurationCheckpoint, который предоставляет опорную точку, с которой приложение может запустить свой таймер.
Поскольку MCU отправляет данные о продолжительности активности, а их передача в приложение может занять некоторое время, ActiveDurationCheckpoint содержит два свойства:
activeDuration– сколько времени длится тренировка.time– время расчета продолжительности активности.
Таким образом, в приложении активная продолжительность тренировки может быть рассчитана на основе ActiveDurationCheckpoint по следующей формуле:
(now() - checkpoint.time) + checkpoint.activeDuration
Это позволяет учесть небольшую разницу между временем, когда активная продолжительность рассчитывается на микроконтроллере и когда она поступает в приложение. Это значение можно использовать для запуска хронометра в приложении и синхронизации таймера приложения со временем в сервисах наблюдения за здоровьем и на микроконтроллере.
Если тренировка приостановлена, приложение ждет, пока расчетное время не превысит отображаемое в интерфейсе, и только после этого перезапускает таймер.
Это связано с тем, что сигнал о паузе доходит до сервисов наблюдения за здоровьем и микроконтроллера с небольшой задержкой. Например, если приложение приостановлено в момент времени t=10 секунд, сервисы наблюдения за здоровьем могут не передать обновление PAUSED в приложение до момента времени t=10,2 секунды.
Как работать с данными из ExerciseClient
Показатели для типов данных, на которые зарегистрировано ваше приложение, передаются в сообщениях ExerciseUpdate.
Процессор доставляет сообщения, только когда он активен или когда достигнут максимальный период отчетности, например каждые 150 секунд. Не полагайтесь на частоту ExerciseUpdate, чтобы продвигать хронометр с помощью activeDuration. Пример реализации независимого хронометра можно найти в примере упражнения на GitHub.
Когда пользователь начинает тренировку, сообщения ExerciseUpdate могут доставляться часто, например каждую секунду. Когда пользователь начинает тренировку, экран может выключиться. В этом случае сервисы наблюдения за здоровьем могут реже передавать данные, но частота выборки останется прежней, чтобы не активировать основной процессор. Когда пользователь смотрит на экран, все данные, находящиеся в процессе пакетной обработки, немедленно доставляются в ваше приложение.
Как управлять частотой пакетной обработки
В некоторых случаях вам может потребоваться контролировать частоту, с которой приложение получает данные определенных типов, когда экран выключен. Объект BatchingMode позволяет приложению переопределить стандартное поведение пакетной обработки, чтобы получать данные чаще.
Чтобы настроить частоту пакетной обработки, выполните следующие действия:
Проверьте, поддерживается ли устройством определенное разрешение
BatchingMode:// 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 }Укажите, что объект
ExerciseConfigдолжен использовать определенный объектBatchingMode, как показано в следующем фрагменте кода.val config = ExerciseConfig( exerciseType = ExerciseType.WORKOUT, dataTypes = setOf( DataType.HEART_RATE_BPM, DataType.TOTAL_CALORIES ), // ... batchingModeOverrides = setOf(BatchingMode.HEART_RATE_5_SECONDS) )При необходимости вы можете динамически настраивать
BatchingModeво время тренировки, а не использовать определенный режим пакетной передачи на протяжении всей тренировки:val desiredModes = setOf(BatchingMode.HEART_RATE_5_SECONDS) exerciseClient.overrideBatchingModesForActiveExercise(desiredModes)Чтобы удалить настроенный параметр
BatchingModeи вернуться к режиму работы по умолчанию, передайте пустой набор вexerciseClient.overrideBatchingModesForActiveExercise().
Временные метки
Каждая точка данных представляет собой время, прошедшее с момента загрузки устройства. Чтобы преобразовать это значение во временную метку, выполните следующие действия:
val bootInstant =
Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime())
Затем это значение можно использовать с getStartInstant() или getEndInstant() для каждого элемента данных.
Точность данных
Для некоторых типов данных можно указать точность каждого элемента.
Это значение указывается в свойстве accuracy.
Классы HrAccuracy и LocationAccuracy можно заполнить для типов данных HEART_RATE_BPM и LOCATION соответственно. Если свойство accuracy доступно, используйте его, чтобы определить, достаточно ли точен каждый элемент данных для вашего приложения.
Хранение и загрузка данных
Используйте Room, чтобы сохранять данные, полученные от сервисов наблюдения за здоровьем. Данные загружаются в конце тренировки с помощью такого механизма, как Work Manager. Это помогает убедиться, что сетевые вызовы для загрузки данных откладываются до окончания тренировки, минимизируя энергопотребление во время тренировки и упрощая работу.
Контрольный список интеграции
Прежде чем публиковать приложение, использующее ExerciseClient сервисов наблюдения за здоровьем, ознакомьтесь с контрольным списком ниже. Он поможет вам избежать распространенных проблем. Убедитесь, что:
- Приложение проверяет возможности типа тренировки и устройства при каждом запуске. Так вы сможете определить, когда определенное устройство или тренировка не поддерживает один из типов данных, необходимых вашему приложению.
- Вы запрашиваете и поддерживаете необходимые разрешения и указываете их в файле манифеста. Перед вызовом
prepareExerciseAsync()приложение проверяет, предоставлены ли динамические разрешения. - Ваше приложение использует
getCurrentExerciseInfoAsync()для обработки следующих случаев:- Тренировка уже отслеживается, и приложение перезаписывает предыдущую тренировку.
- Другое приложение завершило тренировку. Это может произойти, когда пользователь снова открывает приложение, он видит сообщение, объясняющее, что упражнение остановлено, потому что другое приложение взяло на себя управление.
- Если вы используете данные
LOCATION:- Приложение поддерживает
ForegroundServiceс соответствующимforegroundServiceTypeна протяжении всей тренировки (включая вызов prepare). - Проверяет, включен ли GPS на устройстве, с помощью
isProviderEnabled(LocationManager.GPS_PROVIDER)и при необходимости предлагает пользователю открыть настройки геолокации. - Если вам нужно получать данные о местоположении с низкой задержкой, интегрируйте Fused Location Provider (FLP) и используйте его данные для первоначального определения местоположения. Если сервисы наблюдения за здоровьем предоставляют более стабильные геоданные, используйте их вместо FLP.
- Приложение поддерживает
- Если вашему приложению требуется загрузка данных, все сетевые вызовы для загрузки данных откладываются до окончания тренировки. В противном случае на протяжении всего упражнения приложение будет выполнять необходимые сетевые вызовы экономно.
Рекомендуем
- Примечание. Текст ссылки показывается, когда JavaScript отключен.
- Пассивное обновление данных
- Сервисы наблюдения за здоровьем в Wear OS
- Как начать работу с карточками