이 가이드는 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
앱에 계획된 운동 기능을 추가하려면 먼저 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
를 사용하여 이러한 권한을 요청합니다. 헬스 커넥트 권한 화면이 표시됩니다.
// 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)
}
}
사용자는 언제든지 권한을 부여하거나 취소할 수 있으므로 앱은 부여된 권한을 주기적으로 확인하고 권한이 손실된 시나리오를 처리해야 합니다.
관련 권한
훈련 계획은 운동 세션에 연결됩니다. 따라서 사용자는 헬스 커넥트의 이 기능을 최대한 활용하기 위해 운동 계획과 관련된 각 기록 유형을 사용할 권한을 부여해야 합니다.
예를 들어 러닝 시리즈 중에 사용자의 심박수를 측정하는 운동 계획이 있다면 운동 세션을 작성하고 나중에 평가할 결과를 읽기 위해 개발자가 다음 권한을 선언하고 사용자가 부여해야 할 수 있습니다.
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 |
계획된 운동 세션 기록에 포함된 정보
- 세션 제목입니다.
- 계획된 운동 블록 목록입니다.
- 세션의 시작 및 종료 시간입니다.
- 운동 유형입니다.
- 활동에 대한 메모입니다.
- 메타데이터
- 완료된 운동 세션 ID: 이 값은 이 계획된 운동 세션과 관련된 운동 세션이 완료된 후 자동으로 작성됩니다.
계획된 운동 블록 레코드에 포함된 정보
계획된 운동 블록에는 다양한 단계 그룹의 반복을 지원하는 운동 단계 목록이 포함됩니다 (예: 팔 컬, 버피, 크런치를 연속으로 5회 실행).
- 차단에 대한 설명입니다.
- 계획된 운동 단계 목록입니다.
- 반복 횟수입니다.
계획된 운동 단계 기록에 포함된 정보
지원되는 집계
이 데이터 유형에 지원되는 집계가 없습니다.
사용 예
사용자가 이틀 후에 90분간의 러닝을 계획한다고 가정해 보겠습니다. 이 러닝은 호수 주변을 3바퀴 돌며 목표 심박수는 90~110bpm입니다.
- 다음과 같은 계획된 운동 세션은 사용자가 운동 계획 앱에서 정의합니다.
- 러닝의 계획된 시작 및 종료
- 운동 유형 (러닝)
- 랩 수 (반복)
- 심박수 성능 목표 (90~110bpm)
- 이 정보는 운동 블록과 단계로 그룹화되고 트레이닝 계획 앱에 의해 헬스 커넥트에
PlannedExerciseSessionRecord
로 기록됩니다. - 사용자가 계획된 세션을 실행합니다 (러닝).
- 세션과 관련된 운동 데이터는 다음 중 하나로 기록됩니다.
- 세션 중에 웨어러블 기기에서 예를 들어 심박수입니다.
이 데이터는 활동의 레코드 유형으로 헬스 커넥트에 기록됩니다. 이 경우
HeartRateRecord
입니다. - 세션 후 사용자가 수동으로 예를 들어 실제 러닝의 시작과 끝을 표시합니다. 이 데이터는 헬스 커넥트에
ExerciseSessionRecord
로 작성됩니다.
- 세션 중에 웨어러블 기기에서 예를 들어 심박수입니다.
이 데이터는 활동의 레코드 유형으로 헬스 커넥트에 기록됩니다. 이 경우
- 나중에 트레이닝 계획 앱은 헬스 커넥트에서 데이터를 읽어 계획된 운동 세션에서 사용자가 설정한 목표에 대한 실제 성과를 평가합니다.
운동 계획 및 목표 설정
사용자는 향후 운동을 계획하고 목표를 설정할 수 있습니다. 이를 헬스 커넥트에 계획된 운동 세션으로 작성합니다.
사용 예에 설명된 예에서 사용자는 이틀 후 90분간의 러닝을 계획합니다. 이 러닝은 호수 주변을 3바퀴 돌며 목표 심박수는 90~110bpm입니다.
이와 같은 스니펫은 계획된 운동 세션을 헬스 커넥트에 기록하는 앱의 양식 핸들러에서 찾을 수 있습니다. 학습을 제공하는 서비스와의 통합을 위한 수집 지점에서도 찾을 수 있습니다.
// 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()
운동 및 활동 데이터 기록
이틀 후 사용자가 실제 운동 세션을 기록합니다. 이를 헬스 커넥트에 운동 세션으로 작성합니다.
이 예에서는 사용자의 세션 지속 시간이 계획된 지속 시간과 정확히 일치합니다.
다음 스니펫은 운동 세션을 헬스 커넥트에 기록하는 앱의 양식 핸들러에서 찾을 수 있습니다. 운동 세션을 감지하고 기록할 수 있는 웨어러블의 데이터 수집 및 내보내기 핸들러에서도 찾을 수 있습니다.
여기서 insertedPlannedExerciseSessionId
는 이전 예시에서 재사용됩니다. 실제 앱에서는 사용자가 기존 세션 목록에서 계획된 운동 세션을 선택하여 ID가 결정됩니다.
// 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))
웨어러블은 러닝 중 심박수도 기록합니다. 다음 스니펫을 사용하여 타겟 범위 내에서 레코드를 생성할 수 있습니다.
실제 앱에서는 이 스니펫의 기본 부분이 웨어러블에서 온 메시지의 핸들러에 있을 수 있습니다. 이 핸들러는 수집 시 헬스 커넥트에 측정을 씁니다.
// 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
)