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

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

См. пример упражнения на GitHub.

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

Чтобы добавить зависимость от Health Services, необходимо добавить в свой проект репозиторий Google Maven. Дополнительную информацию можно найти в репозитории Google Maven .

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

классный

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

Котлин

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

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

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

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

Каждый 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() , проверьте следующее:

  • Проверьте настройки местоположения для всей платформы. Пользователь управляет этой настройкой в ​​главном меню «Настройки»; это отличается от проверки разрешений на уровне приложения.

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

  • Убедитесь, что у вашего приложения есть разрешения во время выполнения для датчиков тела, распознавания активности и определения точного местоположения. В случае отсутствия разрешений запросите у пользователя разрешения во время выполнения, предоставив соответствующий контекст. Если пользователь не предоставляет определенное разрешение, удалите типы данных, связанные с этим разрешением, из вызова prepareExerciseAsync() . Если не предоставлены ни датчик тела, ни разрешения на определение местоположения, не вызывайте prepareExerciseAsync() , так как вызов подготовки предназначен специально для получения стабильной частоты пульса или определения координат GPS перед началом упражнения. Приложение по-прежнему может получать данные о расстоянии, темпе, скорости и других показателях на основе количества шагов, для которых эти разрешения не требуются.

Выполните следующие действия, чтобы гарантировать успешный вызов prepareExerciseAsync() :

  • Используйте AmbientLifecycleObserver для действия перед тренировкой, которое содержит вызов подготовки.
  • Вызовите prepareExerciseAsync() из службы переднего плана. Если он не находится в службе и привязан к жизненному циклу активности, то подготовка датчика может быть без необходимости прекращена.
  • Вызовите endExercise() , чтобы отключить датчики и снизить энергопотребление, если пользователь отходит от занятий перед тренировкой.

В следующем примере показано, как вызватьprepreExersizedAsync 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.

Когда приложение находится в состоянии 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()
}

Вы также можете отмечать круги для всех упражнений. Службы Health Services предоставляют ExerciseLapSummary с показателями, агрегированными за период круга.

В предыдущем примере показано использование isGpsEnabled , которое должно быть истинным при запросе данных о местоположении. Однако использование GPS может помочь и с другими показателями. Если ExerciseConfig в качестве DataType указано расстояние, по умолчанию используются шаги для оценки расстояния. Если дополнительно включить GPS, вместо этого можно использовать информацию о местоположении для оценки расстояния.

Пауза, возобновление и завершение тренировки

Вы можете приостанавливать, возобновлять и завершать тренировки, используя соответствующий метод, например, pauseExerciseAsync() или endExerciseAsync() .

Используйте состояние из ExerciseUpdate в качестве источника истины. Тренировка не считается приостановленной, когда возвращается вызов метода pauseExerciseAsync() , а когда это состояние отражается в сообщении ExerciseUpdate . Это особенно важно учитывать, когда речь идет о состояниях пользовательского интерфейса. Если пользователь нажимает паузу, отключите кнопку паузы и вызовите pauseExerciseAsync() в службах здравоохранения. Подождите, пока Службы здравоохранения перейдут в состояние паузы с помощью ExerciseUpdate.exerciseStateInfo.state , а затем переключите кнопку для возобновления. Это связано с тем, что доставка обновлений состояния Health Services может занять больше времени, чем нажатие кнопки, поэтому, если вы привяжете все изменения пользовательского интерфейса к нажатиям кнопок, пользовательский интерфейс может не синхронизироваться с состоянием Health Services.

Имейте это в виду в следующих ситуациях:

  • Включена автоматическая пауза: тренировку можно приостановить или начать без вмешательства пользователя.
  • Другое приложение начинает тренировку: ваша тренировка может быть прервана без вмешательства пользователя.

Если тренировка вашего приложения прерывается другим приложением, ваше приложение должно корректно обработать это завершение:

  • Сохраните состояние частичной тренировки, чтобы прогресс пользователя не был стерт.
  • Удалите значок «Текущая активность» и отправьте пользователю уведомление о том, что его тренировка была завершена другим приложением.

Кроме того, обработайте случай, когда разрешения отменяются во время текущего упражнения. Оно отправляется с использованием состояния 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) устройства — маломощный процессор, отвечающий за отслеживание тренировок — все должны быть синхронизированы и иметь одинаковую текущую продолжительность активности. Чтобы справиться с этим, службы Health Services отправляют ActiveDurationCheckpoint , предоставляющую точку привязки, с которой приложение может запустить свой таймер.

Поскольку активная продолжительность отправляется из MCU и ее доставка в приложение может занять небольшое время, ActiveDurationCheckpoint содержит два свойства:

  • activeDuration : как долго упражнение было активным
  • time : когда была рассчитана активная продолжительность

Таким образом, в приложении активную продолжительность тренировки можно рассчитать по ActiveDurationCheckpoint используя следующее уравнение:

(сейчас() - checkpoint.time) + checkpoint.activeDuration

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

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

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

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

Процессор доставляет сообщения только при пробуждении или при достижении максимального периода отчетности, например каждые 150 секунд. Не полагайтесь на частоту ExerciseUpdate для продвижения хронометра с помощью activeDuration . См. пример упражнения на 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 для сохранения данных, полученных от Health Services. Загрузка данных происходит в конце упражнения с использованием такого механизма, как Work Manager . Это гарантирует, что сетевые вызовы для загрузки данных будут отложены до завершения тренировки, что сводит к минимуму энергопотребление во время тренировки и упрощает работу.

Контрольный список интеграции

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

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