Планы тренировок

Это руководство совместимо с Health Connect версии 1.1.0-alpha11 .

Health Connect предоставляет тип данных о запланированных тренировках , позволяющий приложениям для тренировок составлять планы тренировок и читать их. Записанные упражнения (тренировки) можно считывать для персонализированного анализа эффективности, помогая пользователям достигать своих тренировочных целей.

Доступность функций

Чтобы определить, поддерживает ли устройство пользователя планы тренировок в Health Connect, проверьте наличие FEATURE_PLANNED_EXERCISE на клиенте:

if (healthConnectClient
     .features
     .getFeatureStatus(
       HealthConnectFeatures.FEATURE_PLANNED_EXERCISE
     ) == HealthConnectFeatures.FEATURE_STATUS_AVAILABLE) {

  // Feature is available
} else {
  // Feature isn't available
}

Более подробную информацию см. в разделе Проверка доступности функции .

Требуемые разрешения

Доступ к запланированным учениям защищен следующими разрешениями:

  • android.permission.health.READ_PLANNED_EXERCISE
  • android.permission.health.WRITE_PLANNED_EXERCISE

Чтобы добавить в приложение возможность плановых тренировок, начните с запроса разрешений на запись для типа данных PlannedExerciseSession .

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

<application>
  <uses-permission
android:name="android.permission.health.WRITE_PLANNED_EXERCISE" />
...
</application>

Чтобы прочитать запланированное упражнение, вам необходимо запросить следующие разрешения:

<application>
  <uses-permission
android:name="android.permission.health.READ_PLANNED_EXERCISE" />
...
</application>

Запросить разрешения у пользователя

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

Для этого создайте набор разрешений для необходимых типов данных. Убедитесь, что разрешения в наборе предварительно объявлены в манифесте Android.

// Create a set of permissions for required data types
val PERMISSIONS =
    setOf(
  HealthPermission.getReadPermission(HeartRateRecord::class),
  HealthPermission.getWritePermission(HeartRateRecord::class),
  HealthPermission.getReadPermission(PlannedExerciseSessionRecord::class),
  HealthPermission.getWritePermission(PlannedExerciseSessionRecord::class),
  HealthPermission.getReadPermission(ExerciseSessionRecord::class),
  HealthPermission.getWritePermission(ExerciseSessionRecord::class)
)

Используйте getGrantedPermissions , чтобы проверить, предоставлены ли приложению необходимые разрешения. Если нет, запросите их с помощью createRequestPermissionResultContract . Откроется экран разрешений Health Connect.

// Create the permissions launcher
val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract()

val requestPermissions = registerForActivityResult(requestPermissionActivityContract) { granted ->
  if (granted.containsAll(PERMISSIONS)) {
    // Permissions successfully granted
  } else {
    // Lack of required permissions
  }
}

suspend fun checkPermissionsAndRun(healthConnectClient: HealthConnectClient) {
  val granted = healthConnectClient.permissionController.getGrantedPermissions()
  if (granted.containsAll(PERMISSIONS)) {
    // Permissions already granted; proceed with inserting or reading data
  } else {
    requestPermissions.launch(PERMISSIONS)
  }
}

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

Планы тренировок привязаны к сеансам тренировок . Поэтому для полноценного использования этой функции Health Connect пользователю необходимо дать разрешение на использование каждого типа записи, связанного с планом тренировок.

Например, если план тренировок измеряет частоту сердечных сокращений пользователя во время серии пробежек, разработчику может потребоваться объявить следующие разрешения и предоставить их пользователю, чтобы записать сеанс тренировки и прочитать результаты для последующей оценки:

  • android.permission.health.READ_EXERCISE
  • android.permission.health.READ_EXERCISE_ROUTES
  • android.permission.health.READ_HEART_RATE
  • android.permission.health.WRITE_EXERCISE
  • android.permission.health.WRITE_EXERCISE_ROUTE
  • android.permission.health.WRITE_HEART_RATE

Однако часто приложение, которое создаёт планы тренировок и сравнивает результаты с ними, отличается от приложения, которое использует планы тренировок и записывает данные о фактических упражнениях. В зависимости от типа приложения могут потребоваться не все разрешения на чтение и запись. Например, для каждого типа приложения могут потребоваться только следующие разрешения:

Приложение для планирования тренировок Приложение для тренировок
WRITE_PLANNED_EXERCISE READ_PLANNED_EXERCISE
READ_EXERCISE WRITE_EXERCISE
READ_EXERCISE_ROUTES WRITE_EXERCISE_ROUTE
READ_HEART_RATE WRITE_HEART_RATE

Информация, включенная в запись запланированного сеанса тренировки

  • Название сессии.
  • Список запланированных блоков упражнений .
  • Время начала и окончания сеанса.
  • Тип упражнения.
  • Примечания к занятию.
  • Метаданные.
  • Идентификатор завершенного сеанса упражнений — записывается автоматически после завершения сеанса упражнений, связанного с данным запланированным сеансом упражнений.

Информация, включенная в запись запланированного блока упражнений

Запланированный блок упражнений содержит список этапов упражнений для обеспечения повторения различных групп шагов (например, выполните последовательность сгибаний рук, отжиманий и скручиваний пять раз подряд).

Информация, включенная в запись запланированных шагов упражнений

Поддерживаемые агрегации

Для этого типа данных не поддерживаются агрегации.

Пример использования

Предположим, пользователь планирует 90-минутную пробежку через два дня. Эта пробежка будет состоять из трёх кругов вокруг озера с целевой частотой пульса от 90 до 110 ударов в минуту.

  1. Пользователь в приложении плана тренировок определяет запланированную сессию тренировок со следующими параметрами:
    1. Планируемое начало и конец пробега
    2. Вид упражнения (бег)
    3. Количество кругов (повторений)
    4. Целевой показатель частоты сердечных сокращений (от 90 до 110 ударов в минуту)
  2. Эта информация группируется в блоки упражнений и шаги и записывается в Health Connect приложением плана тренировок как PlannedExerciseSessionRecord .
  3. Пользователь выполняет запланированный сеанс (бег).
  4. Данные об упражнениях, относящиеся к сеансу, записываются:
    1. С помощью носимого устройства во время сеанса. Например, пульс. Эти данные записываются в Health Connect как тип записи для активности. В данном случае — HeartRateRecord .
    2. Вручную пользователем после сеанса. Например, для указания времени начала и окончания пробежки. Эти данные записываются в Health Connect как ExerciseSessionRecord .
  5. Позднее приложение плана тренировок считывает данные из Health Connect, чтобы оценить фактические результаты и сравнить их с целями, установленными пользователем в запланированном сеансе тренировок.

Планируйте упражнения и ставьте цели

Пользователь может планировать свои тренировки на будущее и ставить цели. Запишите это в Health Connect как запланированную тренировку .

В примере, описанном в разделе «Пример использования» , пользователь планирует 90-минутную пробежку через два дня. Эта пробежка будет состоять из трёх кругов вокруг озера с целевым пульсом от 90 до 110 ударов в минуту.

Подобный фрагмент кода можно найти в обработчике форм приложения, которое регистрирует запланированные тренировки в Health Connect. Его также можно найти в точке входа для интеграции, например, с сервисом, предлагающим тренировки.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()
if (!grantedPermissions.contains(
      HealthPermission.getWritePermission(PlannedExerciseSessionRecord::class))) {
    // The user hasn't granted the app permission to write planned exercise session data.
    return
}

val plannedDuration = Duration.ofMinutes(90)
val plannedStartDate = LocalDate.now().plusDays(2)

val plannedExerciseSessionRecord = PlannedExerciseSessionRecord(
    startDate = plannedStartDate,
    duration = plannedDuration,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    blocks = listOf(
        PlannedExerciseBlock(
            repetitions = 1, steps = listOf(
                PlannedExerciseStep(
                    exerciseType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
                    exercisePhase = PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
                    completionGoal = ExerciseCompletionGoal.RepetitionsGoal(repetitions = 3),
                    performanceTargets = listOf(
                        ExercisePerformanceTarget.HeartRateTarget(
                            minHeartRate = 90.0, maxHeartRate = 110.0
                        )
                    )
                ),
            ), description = "Three laps around the lake"
        )
    ),
    title = "Run at lake",
    notes = null,
    metadata = Metadata.manualEntry(
      device = Device(type = Device.Companion.TYPE_PHONE)
    )
)
val insertedPlannedExerciseSessions =
    healthConnectClient.insertRecords(listOf(plannedExerciseSessionRecord)).recordIdsList
val insertedPlannedExerciseSessionId = insertedPlannedExerciseSessions.first()

Данные о тренировках и активности

Через два дня пользователь регистрирует сам сеанс тренировки. Запишите это в Health Connect как сеанс тренировки .

В этом примере продолжительность сеанса пользователя точно совпала с запланированной.

Следующий фрагмент кода можно найти в обработчике форм приложения, которое регистрирует сеансы тренировок в Health Connect. Его также можно найти в обработчиках сбора и экспорта данных для носимых устройств, способных обнаруживать и регистрировать сеансы тренировок.

В данном случае insertedPlannedExerciseSessionId используется повторно из предыдущего примера. В реальном приложении идентификатор определялся бы пользователем, выбирающим запланированную тренировку из списка существующих.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()
if (!grantedPermissions.contains(
      HealthPermission.getWritePermission(ExerciseSessionRecord::class))) {
    // The user doesn't granted the app permission to write exercise session data.
    return
}

val sessionDuration = Duration.ofMinutes(90)
val sessionEndTime = Instant.now()
val sessionStartTime = sessionEndTime.minus(sessionDuration)

val exerciseSessionRecord = ExerciseSessionRecord(
    startTime = sessionStartTime,
    startZoneOffset = ZoneOffset.UTC,
    endTime = sessionEndTime,
    endZoneOffset = ZoneOffset.UTC,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    segments = listOf(
        ExerciseSegment(
            startTime = sessionStartTime,
            endTime = sessionEndTime,
            repetitions = 3,
            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING
        )
    ),
    title = "Run at lake",
    plannedExerciseSessionId = insertedPlannedExerciseSessionId,
    metadata = Metadata.manualEntry(
      device = Device(type = Device.Companion.TYPE_PHONE)
    )
)
val insertedExerciseSessions =
    healthConnectClient.insertRecords(listOf(exerciseSessionRecord))

Носимое устройство также регистрирует пульс во время пробежки. Следующий фрагмент кода можно использовать для создания записей в пределах целевого диапазона.

В реальном приложении основные части этого фрагмента можно найти в обработчике сообщений от носимого устройства, который будет записывать измерения в Health Connect после их получения.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()
if (!grantedPermissions.contains(
      HealthPermission.getWritePermission(HeartRateRecord::class))) {
    // The user doesn't granted the app permission to write heart rate record data.
    return
}

val samples = mutableListOf<HeartRateRecord.Sample>()
var currentTime = sessionStartTime
while (currentTime.isBefore(sessionEndTime)) {
    val bpm = Random.nextInt(21) + 90
    val heartRateRecord = HeartRateRecord.Sample(
        time = currentTime,
        beatsPerMinute = bpm.toLong(),
    )
    samples.add(heartRateRecord)
    currentTime = currentTime.plusSeconds(180)
}

val heartRateRecord = HeartRateRecord(
    startTime = sessionStartTime,
    startZoneOffset = ZoneOffset.UTC,
    endTime = sessionEndTime,
    endZoneOffset = ZoneOffset.UTC,
    samples = samples,
    metadata = Metadata.autoRecorded(
      device = Device(type = Device.Companion.TYPE_WATCH)
    )
)
val insertedHeartRateRecords = healthConnectClient.insertRecords(listOf(heartRateRecord))

Оценить целевые показатели эффективности

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

Подобный фрагмент кода, скорее всего, можно найти в периодическом задании по оценке целевых показателей эффективности или при загрузке списка упражнений и отображении уведомления о целевых показателях эффективности в приложении.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
     healthConnectClient.permissionController.getGrantedPermissions()
if (!grantedPermissions.containsAll(
        listOf(
            HealthPermission.getReadPermission(ExerciseSessionRecord::class),
            HealthPermission.getReadPermission(PlannedExerciseSessionRecord::class),
            HealthPermission.getReadPermission(HeartRateRecord::class)
        )
    )
) {
    // The user doesn't granted the app permission to read exercise session record data.
    return
}

val searchDuration = Duration.ofDays(1)
val searchEndTime = Instant.now()
val searchStartTime = searchEndTime.minus(searchDuration)

val response = healthConnectClient.readRecords(
    ReadRecordsRequest<ExerciseSessionRecord>(
        timeRangeFilter = TimeRangeFilter.between(searchStartTime, searchEndTime)
    )
)
for (exerciseRecord in response.records) {
    val plannedExerciseRecordId = exerciseRecord.plannedExerciseSessionId
    val plannedExerciseRecord =
        if (plannedExerciseRecordId == null) null else healthConnectClient.readRecord(
            PlannedExerciseSessionRecord::class, plannedExerciseRecordId
        ).record
    if (plannedExerciseRecord != null) {
        val aggregateRequest = AggregateRequest(
            metrics = setOf(HeartRateRecord.BPM_AVG),
            timeRangeFilter = TimeRangeFilter.between(
                exerciseRecord.startTime, exerciseRecord.endTime
            ),
        )
        val aggregationResult = healthConnectClient.aggregate(aggregateRequest)

        val maxBpm = aggregationResult[HeartRateRecord.BPM_MAX]
        val minBpm = aggregationResult[HeartRateRecord.BPM_MIN]
        if (maxBpm != null && minBpm != null) {
            plannedExerciseRecord.blocks.forEach { block ->
                block.steps.forEach { step ->
                    step.performanceTargets.forEach { target ->
                        when (target) {
                            is ExercisePerformanceTarget.HeartRateTarget -> {
                                val minTarget = target.minHeartRate
                                val maxTarget = target.maxHeartRate
                                if(
                                    minBpm >= minTarget && maxBpm <= maxTarget
                                ) {
                                  // Success!
                                }
                            }
                            // Handle more target types
                            }
                        }
                    }
                }
            }
        }
    }
}

Сеансы упражнений

Тренировки могут включать в себя все, что угодно: от бега до бадминтона.

Запишите сеансы упражнений

Вот как создать запрос на вставку, включающий сеанс:

suspend fun writeExerciseSession(healthConnectClient: HealthConnectClient) {
    healthConnectClient.insertRecords(
        listOf(
            ExerciseSessionRecord(
                startTime = START_TIME,
                startZoneOffset = START_ZONE_OFFSET,
                endTime = END_TIME,
                endZoneOffset = END_ZONE_OFFSET,
                exerciseType = ExerciseSessionRecord.ExerciseType.RUNNING,
                title = "My Run"
            ),
            // ... other records
        )
    )
}

Прочитайте сеанс упражнений

Вот пример того, как читать сеанс упражнений:

suspend fun readExerciseSessions(
    healthConnectClient: HealthConnectClient,
    startTime: Instant,
    endTime: Instant
) {
    val response =
        healthConnectClient.readRecords(
            ReadRecordsRequest(
                ExerciseSessionRecord::class,
                timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
            )
        )
    for (exerciseRecord in response.records) {
        // Process each exercise record
        // Optionally pull in with other data sources of the same time range.
        val distanceRecord =
            healthConnectClient
                .readRecords(
                    ReadRecordsRequest(
                        DistanceRecord::class,
                        timeRangeFilter =
                            TimeRangeFilter.between(
                                exerciseRecord.startTime,
                                exerciseRecord.endTime
                            )
                    )
                )
                .records
    }
}

Запись подтипа данных

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

Например, сеансы упражнений могут включать классы ExerciseSegment , ExerciseLap и ExerciseRoute :

val segments = listOf(
  ExerciseSegment(
    startTime = Instant.parse("2022-01-02T10:10:10Z"),
    endTime = Instant.parse("2022-01-02T10:10:13Z"),
    segmentType = ActivitySegmentType.BENCH_PRESS,
    repetitions = 373
  )
)

val laps = listOf(
  ExerciseLap(
    startTime = Instant.parse("2022-01-02T10:10:10Z"),
    endTime = Instant.parse("2022-01-02T10:10:13Z"),
    length = 0.meters
  )
)

ExerciseSessionRecord(
  exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_CALISTHENICS,
    startTime = Instant.parse("2022-01-02T10:10:10Z"),
    endTime = Instant.parse("2022-01-02T10:10:13Z"),
  startZoneOffset = ZoneOffset.UTC,
  endZoneOffset = ZoneOffset.UTC,
  segments = segments,
  laps = laps,
  route = route
)