Запишите упражнение с помощью PracticeClient

Health Services поддерживает приложения для тренировок через ExerciseClient . С помощью ExerciseClient ваше приложение может контролировать ход тренировки, добавлять цели и получать обновления о состоянии тренировки, событиях тренировки и других показателях. Подробнее см. в полном списке типов тренировок , поддерживаемых Health Services.

Посмотрите пример упражнения на GitHub.

Добавить зависимости

Чтобы добавить зависимость от Health Services, необходимо добавить репозиторий Google Maven в свой проект. Подробнее см. в репозитории Google Maven .

Затем в файле build.gradle на уровне модуля добавьте следующую зависимость:

классный

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

Котлин

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

Структура приложения

При создании приложения для тренировок с использованием Health Services используйте следующую структуру приложения:

При подготовке к тренировке и во время неё ваша активность может быть остановлена ​​по разным причинам. Пользователь может переключиться в другое приложение или вернуться к циферблату. Система может отображать что-либо поверх вашей активности, или экран может выключиться после определённого периода бездействия. Используйте постоянно работающую ForegroundService в сочетании с ExerciseClient , чтобы обеспечить корректную работу на протяжении всей тренировки.

Использование ForegroundService позволяет использовать 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() , и для самого занятия. Однако не обновляйте дисплей во время тренировки в режиме AmbientLifecycleObserver: это связано с тем, что службы Health Services собирают данные о тренировке в пакетном режиме, когда экран устройства находится в режиме AmbientLifecycleObserver, для экономии заряда батареи, поэтому отображаемая информация может быть устаревшей. Во время тренировок отображайте данные, которые имеют смысл для пользователя, отображая либо актуальную информацию, либо пустой экран.

Проверить возможности

Каждый 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() , так как вызов preparation предназначен специально для получения стабильного значения пульса или GPS-координат перед началом тренировки. Приложение по-прежнему может получать данные о расстоянии, темпе, скорости и других показателях, для которых эти разрешения не требуются.

Чтобы убедиться, что вызов prepareExerciseAsync() может быть выполнен успешно, выполните следующие действия:

  • Используйте AmbientLifecycleObserver для предтренировочной активности, содержащей вызов подготовки.
  • Вызовите prepareExerciseAsync() из вашей службы переднего плана. Если он не находится в службе и привязан к жизненному циклу активности, то подготовка датчика может быть без необходимости прервана.
  • Вызовите 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 , который должен быть равен true при запросе данных о местоположении. Однако использование 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

Это объясняет небольшую разницу между длительностью активности, рассчитанной в MCU, и её поступлением в приложение. Это можно использовать для установки хронометра в приложение и обеспечения идеальной синхронизации таймера приложения со временем в службах здравоохранения и MCU.

Если упражнение приостановлено, приложение перезапускает таймер в пользовательском интерфейсе только после того, как рассчитанное время превысит отображаемое в нём значение. Это связано с тем, что сигнал о паузе поступает в службы здравоохранения и блок управления микроконтроллерами с небольшой задержкой. Например, если приложение приостановлено в момент времени t=10 секунд, службы здравоохранения могут не отправлять приложению обновление PAUSED до момента t=10,2 секунды.

Работа с данными из ExerciseClient

Метрики для типов данных, для которых зарегистрировано ваше приложение, передаются в сообщениях ExerciseUpdate .

Процессор отправляет сообщения только в состоянии бодрствования или по достижении максимального периода отчётности, например, каждые 150 секунд. Не полагайтесь на частоту ExerciseUpdate для перевода хронометра с activeDuration . См. пример Exercise на GitHub, где показано, как реализовать независимый хронометр.

Когда пользователь начинает тренировку, сообщения ExerciseUpdate могут отправляться часто, например, каждую секунду. Когда пользователь начинает тренировку, экран может выключиться. Службы здравоохранения могут отправлять данные реже, но с той же частотой, чтобы не загружать основной процессор. Когда пользователь смотрит на экран, все данные, находящиеся в процессе пакетной обработки, немедленно передаются в ваше приложение.

Контролируйте скорость дозирования

В некоторых сценариях вам может потребоваться контролировать частоту, с которой ваше приложение получает определённые типы данных при выключенном экране. Объект BatchingMode позволяет вашему приложению переопределить стандартное поведение пакетной обработки, чтобы получать данные чаще.

Чтобы настроить скорость пакетирования, выполните следующие действия:

  1. Проверьте, поддерживается ли конкретное определение 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
    }
    
  2. Укажите, что объект ExerciseConfig должен использовать определенный BatchingMode , как показано в следующем фрагменте кода.

    val config = ExerciseConfig(
        exerciseType = ExerciseType.WORKOUT,
        dataTypes = setOf(
            DataType.HEART_RATE_BPM,
            DataType.TOTAL_CALORIES
        ),
        // ...
        batchingModeOverrides = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    )
    
  3. При желании вы можете динамически настраивать BatchingMode во время тренировки, вместо того чтобы определенный режим пакетной обработки сохранялся на протяжении всей тренировки:

    val desiredModes = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    exerciseClient.overrideBatchingModesForActiveExercise(desiredModes)
    
  4. Чтобы очистить настроенный 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 от Health Services, ознакомьтесь со следующим контрольным списком, чтобы убедиться, что пользовательский интерфейс позволяет избежать некоторых распространённых проблем. Убедитесь, что:

  • Ваше приложение проверяет возможности типа упражнения и устройства при каждом запуске. Таким образом, вы можете определить, когда конкретное устройство или упражнение не поддерживает один из типов данных, необходимых вашему приложению.
  • Вы запрашиваете и поддерживаете необходимые разрешения, указывая их в файле манифеста. Перед вызовом prepareExerciseAsync() ваше приложение подтверждает предоставление разрешений во время выполнения.
  • Ваше приложение использует getCurrentExerciseInfoAsync() для обработки случаев, когда :
    • Упражнение уже отслеживается, и ваше приложение перезаписывает предыдущее упражнение.
    • Другое приложение прервало вашу тренировку. Это может произойти, когда пользователь снова открывает приложение и видит сообщение о том, что тренировка остановлена ​​из-за того, что другое приложение перехватило управление.
  • Если вы используете данные LOCATION :
    • Ваше приложение поддерживает ForegroundService с соответствующим foregroundServiceType на протяжении всего упражнения (включая вызов подготовки).
    • Проверьте, включен ли GPS на устройстве, с помощью isProviderEnabled(LocationManager.GPS_PROVIDER) и при необходимости предложите пользователю открыть настройки местоположения.
    • В сложных случаях, когда получение данных о местоположении с минимальной задержкой имеет решающее значение, рассмотрите возможность интеграции поставщика объединённого местоположения (FLP) и использования его данных для первоначального определения местоположения. Если от служб здравоохранения доступна более стабильная информация о местоположении, используйте её вместо FLP.
  • Если вашему приложению требуется загрузка данных, любые сетевые вызовы для загрузки данных откладываются до завершения упражнения. В противном случае, на протяжении всего упражнения ваше приложение будет выполнять все необходимые сетевые вызовы экономно.
{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}