планы тренировок;

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

Проверьте доступность платформы "Здоровье и спорт"

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

Как проверить, доступна ли платформа "Здоровье и спорт"

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(), вы можете предложить пользователю установить или обновить приложение "Здоровье и спорт" из Google Play.

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

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

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, чтобы запросить эти разрешения. Откроется экран с разрешениями для сервиса "Здоровье и спорт".
val permissions = setOf(
        HealthPermission.getReadPermission(StepsRecord::class),
        HealthPermission.getWritePermission(StepsRecord::class),
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getWritePermission(HeartRateRecord::class)
    )

val requestPermissionsLauncher = rememberLauncherForActivityResult(
    contract = PermissionController.createRequestPermissionResultContract()
) { grantedPermissions ->
    if (grantedPermissions.containsAll(permissions)) {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions granted!") }
    } else {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions denied.") }
    }
}
Поскольку пользователи могут в любой момент предоставить или отозвать разрешения, ваше приложение должно проверять их каждый раз перед использованием и обрабатывать ситуации, когда разрешение теряется.

Планы тренировок связаны с тренировками. Поэтому, чтобы использовать эту функцию 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. Эта информация группируется в блоки упражнений и шагов и записывается в приложение "Здоровье и спорт" как PlannedExerciseSessionRecord.
  3. Пользователь выполняет запланированный сеанс (выполняется).
  4. Данные о тренировках, связанных с сеансом, записываются:
    1. носимым устройством во время сеанса. например пульс. Эти данные записываются на платформу "Здоровье и спорт" как тип записи для активности. В этом случае HeartRateRecord.
    2. вручную пользователем после сеанса. Например, можно указать начало и конец фактического пробега. Эти данные записываются в сервис "Здоровье и спорт" как ExerciseSessionRecord.
  5. Позже приложение для тренировок считывает данные из Здоровья и спорта, чтобы оценить фактические результаты по сравнению с целями, заданными пользователем в запланированной тренировке.

Как планировать тренировки и задавать цели

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

В примере использования пользователь планирует пробежку на 90 минут через два дня. Вам предстоит пробежать три круга вокруг озера с целевым пульсом от 90 до 110 ударов в минуту.

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

// 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.
    Log.w("HealthConnect", "Write permission for PlannedExerciseSessionRecord not granted.")
    return
}

val plannedExerciseSessionRecord = PlannedExerciseSessionRecord(
    startTime = startTime,
    endTime = endTime,
    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.activelyRecorded(
        device = Device(type = Device.Companion.TYPE_PHONE),
    ),
    startZoneOffset = null,
    endZoneOffset = null,
)

try {
    // Attempt to insert the record
    val response = healthConnectClient.insertRecords(listOf(plannedExerciseSessionRecord))

    // If execution reaches here, the insert succeeded.
    // Safely extract the ID using firstOrNull()
    val insertedPlannedExerciseSessionId = response.recordIdsList.firstOrNull()

    if (insertedPlannedExerciseSessionId != null) {
        Log.d("HealthConnect", "Successfully inserted planned exercise session ID: $insertedPlannedExerciseSessionId")
    } else {
        Log.w("HealthConnect", "Insertion succeeded but no record IDs were returned.")
    }

} catch (e: Exception) {
    // Handle API failures, database errors, or system issues safely without crashing
    Log.e("HealthConnect", "Failed to insert planned exercise session record", e)
}

Как записывать данные о тренировках и активности

Через два дня пользователь регистрирует фактический сеанс тренировки. Запишите эти данные в сервис "Здоровье и спорт" как сеанс тренировки.

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

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

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.activelyRecorded(
        device = Device(type = Device.Companion.TYPE_PHONE)
    )
)
val insertedExerciseSessions =
    healthConnectClient.insertRecords(listOf(exerciseSessionRecord))

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

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

// 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.activelyRecorded(
        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",
                metadata = Metadata.manualEntry()
            ),
            // ... 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,
  metadata = Metadata.manualEntry()
)