Платформа "Здоровье и спорт" предоставляет тип данных запланированные упражнения, чтобы приложения для тренировок могли записывать планы тренировок, а приложения для занятий спортом – считывать их. Записанные упражнения (тренировки) можно просматривать для персонализированного анализа эффективности, чтобы помочь пользователям достичь целей тренировок.
Проверьте доступность платформы "Здоровье и спорт"
Прежде чем пытаться использовать Здоровье и спорт, ваше приложение должно проверить, доступна ли эта платформа на устройстве пользователя. Приложение "Здоровье и спорт" может быть не предустановлено на некоторых устройствах или отключено.
Проверить доступность можно с помощью метода 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_EXERCISEandroid.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_EXERCISEandroid.permission.health.READ_EXERCISE_ROUTESandroid.permission.health.READ_HEART_RATEandroid.permission.health.WRITE_EXERCISEandroid.permission.health.WRITE_EXERCISE_ROUTEandroid.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 ударов в минуту.
- Пользователь задает в приложении для тренировок запланированное занятие с указанием следующих параметров:
- Планируемые даты начала и окончания выполнения.
- Тип упражнения (бег).
- Количество кругов (повторений)
- Целевой пульс (от 90 до 110 уд/мин).
- Эта информация группируется в блоки упражнений и шагов и записывается в приложение "Здоровье и спорт" как
PlannedExerciseSessionRecord. - Пользователь выполняет запланированный сеанс (выполняется).
- Данные о тренировках, связанных с сеансом, записываются:
- носимым устройством во время сеанса. например пульс.
Эти данные записываются на платформу "Здоровье и спорт" как тип записи для активности. В этом случае
HeartRateRecord. - вручную пользователем после сеанса. Например, можно указать начало и конец фактического пробега. Эти данные записываются в сервис "Здоровье и спорт" как
ExerciseSessionRecord.
- носимым устройством во время сеанса. например пульс.
Эти данные записываются на платформу "Здоровье и спорт" как тип записи для активности. В этом случае
- Позже приложение для тренировок считывает данные из Здоровья и спорта, чтобы оценить фактические результаты по сравнению с целями, заданными пользователем в запланированной тренировке.
Как планировать тренировки и задавать цели
Пользователь может запланировать тренировку на будущее и задать цели. Запишите это в сервис "Здоровье и спорт" как запланированную тренировку.
В примере использования пользователь планирует пробежку на 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()
)