이 가이드는 Health Connect 버전 1.1.0-alpha11과 호환됩니다.
헬스 커넥트는 훈련 앱이 훈련 계획을 작성하고 운동 앱이 훈련 계획을 읽을 수 있도록 하는 계획된 운동 데이터 유형을 제공합니다. 녹음된 운동 (운동)은 사용자가 훈련 목표를 달성하는 데 도움이 되도록 맞춤형 실적 분석을 위해 다시 읽을 수 있습니다.
기능 사용 가능 여부
사용자의 기기가 헬스 커넥트에서 운동 계획을 지원하는지 확인하려면 클라이언트에서 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
앱의 Play Console과 앱 매니페스트에서 다음 권한을 선언합니다.
<application>
<uses-permission
android:name="android.permission.health.READ_PLANNED_EXERCISE" />
<uses-permission
android:name="android.permission.health.WRITE_PLANNED_EXERCISE" />
...
</application>
기기와 앱에서 사용할 모든 적절한 권한을 선언해야 할 책임은 개발자에게 있습니다. 또한 사용하기 전에 각 권한이 사용자에 의해 부여되었는지 확인해야 합니다.
관련 권한
트레이닝 계획은 운동 세션에 연결됩니다. 따라서 헬스 커넥트의 이 기능을 최대한 활용하려면 사용자가 트레이닝 계획과 관련된 각 레코드 유형을 사용할 수 있는 권한을 부여해야 합니다.
예를 들어 트레이닝 계획이 일련의 달리기 중에 사용자의 심박수를 측정하는 경우 운동 세션을 작성하고 나중에 평가할 결과를 읽으려면 개발자가 다음 권한을 선언하고 사용자가 권한을 부여해야 할 수 있습니다.
android.permission.health.READ_EXERCISE
android.permission.health.READ_EXERCISE_ROUTE
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_ROUTE |
WRITE_EXERCISE_ROUTE |
READ_HEART_RATE |
WRITE_HEART_RATE |
계획된 운동 세션 레코드에 포함된 정보
- 세션의 제목입니다.
- 계획된 운동 블록 목록입니다.
- 세션의 시작 및 종료 시간입니다.
- 운동 유형입니다.
- 활동에 대한 메모입니다.
- 메타데이터
- 완료된 운동 세션 ID: 이 계획된 운동 세션과 관련된 운동 세션이 완료된 후 자동으로 작성됩니다.
계획된 운동 블록 레코드에 포함된 정보
계획된 운동 블록에는 다양한 단계 그룹의 반복을 지원하기 위한 운동 단계 목록이 포함됩니다 (예: 팔 컬, 버피, 크런치의 시퀀스를 연속으로 5번 수행).
- 블록에 대한 설명입니다.
- 계획된 운동 단계 목록입니다.
- 반복 횟수입니다.
계획된 운동 걸음 수 기록에 포함된 정보
지원되는 집계
이 데이터 유형에는 지원되는 집계가 없습니다.
사용 예
사용자가 90분 달리기를 계획했다고 가정해 보겠습니다. 이 달리기는 호수를 3바퀴 돌며 심박수를 90~110bpm으로 유지하는 것이 목표입니다.
- 다음과 같은 계획된 운동 세션은 사용자가 트레이닝 계획 앱에서 정의합니다.
- 계획된 실행 시작 및 종료
- 운동 유형 (달리기)
- 랩 수 (반복)
- 심박수의 성능 타겟 (90~110bpm)
- 이 정보는 운동 블록과 단계로 그룹화되며 트레이닝 계획 앱에서 헬스 커넥트에
PlannedExerciseSessionRecord
로 기록됩니다. - 사용자가 계획된 세션을 실행합니다.
- 세션과 관련된 운동 데이터는 다음 중 하나로 기록됩니다.
- 세션 중에 웨어러블에서 예를 들어 심박수입니다.
이 데이터는 활동의 레코드 유형으로 헬스 커넥트에 기록됩니다. 이 경우
HeartRateRecord
입니다. - 세션 종료 후 사용자가 수동으로 예를 들어 실제 실행의 시작과 종료를 표시합니다. 이 데이터는 헬스 커넥트에
ExerciseSessionRecord
로 작성됩니다.
- 세션 중에 웨어러블에서 예를 들어 심박수입니다.
이 데이터는 활동의 레코드 유형으로 헬스 커넥트에 기록됩니다. 이 경우
- 나중에 트레이닝 계획 앱은 헬스 커넥트에서 데이터를 읽어 계획된 운동 세션에서 사용자가 설정한 타겟과 비교하여 실제 실적을 평가합니다.
운동 계획 및 목표 설정
사용자는 향후 운동을 계획하고 목표를 설정할 수 있습니다. 이를 헬스 커넥트에 계획된 운동 세션으로 작성합니다.
사용 예에 설명된 예시에서 사용자는 2일 후 90분 달리기를 계획합니다. 이 달리기는 호수를 3바퀴 돌며 심박수를 90~110bpm으로 유지하는 것이 목표입니다.
이와 같은 스니펫은 계획된 운동 세션을 헬스 커넥트에 기록하는 앱의 양식 핸들러에서 찾을 수 있습니다. 교육을 제공하는 서비스와 같은 통합의 처리 지점에서도 찾을 수 있습니다.
// Ensure 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 = ExerciseSessionRecord.EXERCISE_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
)
val insertedPlannedExerciseSessions =
healthConnectClient.insertRecords(listOf(plannedExerciseSessionRecord)).recordIdsList
val insertedPlannedExerciseSessionId = insertedPlannedExerciseSessions.first()
운동 및 활동 데이터 기록
2일 후 사용자가 실제 운동 세션을 기록합니다. 이를 헬스 커넥트에 운동 세션으로 작성합니다.
이 예에서 사용자의 세션 시간이 계획된 시간과 정확하게 일치했습니다.
다음 스니펫은 헬스 커넥트에 운동 세션을 기록하는 앱의 양식 핸들러에서 찾을 수 있습니다. 운동 세션을 감지하고 기록할 수 있는 웨어러블의 데이터 처리 및 내보내기 핸들러에서도 찾을 수 있습니다.
여기서 insertedPlannedExerciseSessionId
는 이전 예에서 재사용됩니다. 실제 앱에서는 사용자가 기존 세션 목록에서 계획된 운동 세션을 선택하여 ID가 결정됩니다.
// Ensure 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,
)
val insertedExerciseSessions =
healthConnectClient.insertRecords(listOf(exerciseSessionRecord))
또한 웨어러블은 달리기 중 심박수를 기록합니다. 다음 스니펫은 타겟 범위 내에서 레코드를 생성하는 데 사용할 수 있습니다.
실제 앱에서는 이 스니펫의 기본 부분이 수집 시 헬스 커넥트에 측정값을 작성하는 웨어러블의 메시지 핸들러에 있을 수 있습니다.
// Ensure 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,
)
val insertedHeartRateRecords = healthConnectClient.insertRecords(listOf(heartRateRecord))
실적 타겟 평가
사용자가 운동한 다음 날에는 기록된 운동을 검색하고, 계획된 운동 타겟을 확인하고, 추가 데이터 유형을 평가하여 설정된 타겟이 달성되었는지 확인할 수 있습니다.
이와 같은 스니펫은 실적 타겟을 평가하는 주기적 작업이나 연습 목록을 로드하고 앱에서 실적 타겟에 관한 알림을 표시할 때 발견될 수 있습니다.
// Ensure 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
}
}
}
}
}
}
}
}