Это руководство совместимо с платформой "Здоровье и спорт" версии 1.1.0-alpha12 или более поздней.
Платформа "Здоровье и спорт" поддерживает тип данных Запланированная тренировка, чтобы приложения для тренировок могли записывать и считывать планы тренировок. Записанные тренировки можно считывать для персонализированного анализа эффективности, чтобы помочь пользователям достичь своих целей.
Как проверить доступность платформы "Здоровье и спорт"
Прежде чем использовать платформу "Здоровье и спорт", приложение должно проверить, доступна ли она на устройстве пользователя. Платформа "Здоровье и спорт" может быть не предустановлена на некоторых устройствах или отключена.
Проверить доступность можно с помощью метода HealthConnectClient.getSdkStatus().
Как проверить доступность платформы "Здоровье и спорт"
fun checkHealthConnectAvailability(context: Context) { val providerPackageName = "com.google.android.apps.healthdata" 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.") } } }
Связанные разрешения
Планы тренировок связаны с тренировками. Поэтому, чтобы в полной мере использовать эту функцию платформы "Здоровье и спорт", пользователь должен предоставить разрешение на использование каждого типа записей, связанных с планом тренировок.
Например, если план тренировок предусматривает измерение пульса пользователя во время серии пробежек, разработчику может потребоваться объявить следующие разрешения, а пользователю – предоставить их, чтобы можно было записать данные о тренировке и прочитать результаты для последующей оценки:
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))
Носимое устройство также регистрирует частоту сердечных сокращений на протяжении всей пробежки. Для создания записей в целевом диапазоне можно использовать следующий фрагмент кода:
В реальном приложении основные части этого фрагмента могут находиться в обработчике сообщения от носимого устройства, которое записывает измерения в 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.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()
)