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

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

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

Проверьте доступность Health Connect

Прежде чем использовать Health Connect, ваше приложение должно проверить наличие Health Connect на устройстве пользователя. Health Connect может быть предустановлен не на всех устройствах или отключен. Проверить наличие Health Connect можно с помощью метода HealthConnectClient.getSdkStatus() .

Как проверить доступность Health Connect

fun checkHealthConnectAvailability(context: Context) {
    val providerPackageName = "com.google.android.apps.healthdata" // Or get from HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME
    val availabilityStatus = HealthConnectClient.getSdkStatus(context, providerPackageName)

    if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE) {
      // Health Connect is not available. Guide the user to install/enable it.
      // For example, show a dialog.
      return // early return as there is no viable integration
    }
    if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
      // Health Connect is available but requires an update.
      // Optionally redirect to package installer to find a provider, for example:
      val uriString = "market://details?id=$providerPackageName&url=healthconnect%3A%2F%2Fonboarding"
      context.startActivity(
        Intent(Intent.ACTION_VIEW).apply {
          setPackage("com.android.vending")
          data = Uri.parse(uriString)
          putExtra("overlay", true)
          putExtra("callerId", context.packageName)
        }
      )
      return
    }
    // Health Connect is available, obtain a HealthConnectClient instance
    val healthConnectClient = HealthConnectClient.getOrCreate(context)
    // Issue operations with healthConnectClient
}

В зависимости от статуса, возвращаемого getSdkStatus() , вы можете предложить пользователю установить или обновить Health Connect из Google Play Store, если это необходимо.

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

Чтобы определить, поддерживает ли устройство пользователя планы тренировок в 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
)