앱에서 수면 추적 환경을 빌드하려는 경우 헬스 커넥트를 사용하여 다음 작업을 할 수 있습니다.
- 수면 세션 쓰기
- 수면 단계 데이터 쓰기
- 심박수, 산소 포화도, 호흡수와 같은 수면 데이터 쓰기
- 다른 앱의 수면 데이터 읽기
이 가이드에서는 데이터 유형, 백그라운드 실행, 권한, 권장 워크플로, 권장사항을 다루면서 이러한 수면 기능을 빌드하는 방법을 설명합니다.
개요: 포괄적인 수면 추적기 빌드
다음 핵심 단계를 따라 헬스 커넥트를 사용하여 포괄적인 수면 추적 환경을 빌드할 수 있습니다.
- 건강 권한에 따라 권한을 올바르게 구현합니다.
SleepSessionRecord를 사용하여 세션을 녹화합니다.- 세션 중에 수면 단계, 심박수, 산소 포화도와 같은 데이터 유형을 일관되게 작성합니다.
- 밤새 지속적인 데이터 캡처를 확인하기 위해 백그라운드 실행을 적절하게 관리합니다.
- 수면 후 요약 및 분석을 위해 세션 데이터를 읽습니다.
이 워크플로를 사용하면 다른 헬스 커넥트 앱과의 상호 운용이 가능하며 사용자가 관리하는 데이터 액세스를 확인할 수 있습니다.
시작하기 전에
수면 기능을 구현하기 전에 다음을 확인하세요.
- 적절한 종속 항목을 사용하여 헬스 커넥트를 통합합니다.
HealthConnectClient인스턴스를 만듭니다.- 앱이 건강 권한에 기반한 런타임 권한 흐름을 구현하는지 확인합니다.
주요 개념
헬스 커넥트는 몇 가지 핵심 구성요소를 사용하여 수면 데이터를 나타냅니다. SleepSessionRecord는 수면의 중앙 기록 역할을 하며 시작 또는 종료 시간, 수면 단계와 같은 세부정보를 포함합니다. 세션 중에 HeartRateRecord 또는 OxygenSaturationRecord과 같은 다양한 데이터 유형을 기록할 수 있습니다.
수면 세션
수면 데이터는 SleepSessionRecord로 표시됩니다. 각 레코드에는 다음이 저장됩니다.
startTimeendTimestages: 깊은 수면, 얕은 수면, 렘 수면, 깨어 있는 수면을 포함한SleepSessionRecord.Stage목록입니다.- 선택적 세션 메타데이터 (제목, 메모)
앱은 세션과 연결된 여러 데이터 유형을 쓸 수 있습니다.
데이터 유형
수면 세션 중에 기록되는 일반적인 데이터 유형은 다음과 같습니다.
SleepSessionRecord: 깊은 수면, 얕은 수면, 렘 수면, 각성 상태를 포함한 수면 시간과 수면 단계를 기록합니다.HeartRateRecord: 수면 중 심박수를 기록합니다.OxygenSaturationRecord: 수면 중 산소 포화도 (SpO2)를 기록합니다.RespiratoryRateRecord: 수면 중 호흡수를 기록합니다.
각 데이터 유형은 개별 레코드로 저장됩니다.
개발 고려 사항
수면 추적 앱은 화면이 꺼져 있을 때 백그라운드에서 장시간 실행해야 하는 경우가 많습니다. 수면 기능을 빌드할 때는 백그라운드 실행을 관리하고 수면 데이터에 필요한 권한을 요청하는 방법을 고려해야 합니다.
백그라운드 실행
수면 추적 앱은 일반적으로 화면이 꺼진 상태로 밤새 실행됩니다. 이 상태에서는 다음을 사용해야 합니다.
- 데이터 수집을 위한 포그라운드 서비스
- 지연된 쓰기 또는 동기화를 위한
WorkManager - 심박수와 같은 세분화된 데이터의 일반 기록 쓰기를 위한 일괄 처리 전략
모든 쓰기에서 세션 ID를 일관되게 유지하여 연속성을 유지합니다.
권한
앱은 수면 데이터를 읽거나 쓰기 전에 관련 헬스 커넥트 권한을 요청해야 합니다. 데이터 유형의 전체 목록은 헬스 커넥트 데이터 유형을 참고하세요. 수면의 일반적인 권한에는 수면 세션과 심박수 또는 산소 포화도와 같은 측정항목이 포함됩니다.
수면 액세스는 다음 권한으로 보호됩니다.
android.permission.health.READ_SLEEPandroid.permission.health.WRITE_SLEEP
앱에 수면 기능을 추가하려면 먼저 SleepSession 데이터 유형에 대한 권한을 요청하세요.
수면을 쓰기 위해 선언해야 하는 권한은 다음과 같습니다.
<application>
<uses-permission
android:name="android.permission.health.WRITE_SLEEP" />
...
</application>
수면을 읽으려면 다음 권한을 요청해야 합니다.
<application>
<uses-permission
android:name="android.permission.health.READ_SLEEP" />
...
</application>
다음은 심박수, 산소 포화도, 호흡수 데이터가 포함된 수면 세션의 권한을 요청하는 방법을 보여주는 예시입니다.
클라이언트 인스턴스를 만든 후 앱은 사용자에게 권한을 요청해야 합니다. 사용자는 언제든지 권한을 부여하거나 거부할 수 있어야 합니다.
이렇게 하려면 필요한 데이터 유형의 권한 집합을 만드세요. 집합에 있는 권한이 먼저 Android 매니페스트에 선언되어 있는지 확인합니다.
// Create a set of permissions for required data types
val PERMISSIONS =
setOf(
HealthPermission.getReadPermission(SleepSessionRecord::class),
HealthPermission.getWritePermission(SleepSessionRecord::class),
HealthPermission.getReadPermission(HeartRateRecord::class),
HealthPermission.getWritePermission(HeartRateRecord::class),
HealthPermission.getReadPermission(OxygenSaturationRecord::class),
HealthPermission.getWritePermission(OxygenSaturationRecord::class),
HealthPermission.getReadPermission(RespiratoryRateRecord::class),
HealthPermission.getWritePermission(RespiratoryRateRecord::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)
}
}
사용자는 언제든지 권한을 부여하거나 취소할 수 있으므로 앱은 권한을 사용하기 전에 매번 권한을 확인하고 권한이 손실된 시나리오를 처리해야 합니다.
수면 세션 구현
이 섹션에서는 수면 데이터를 기록하기 위한 권장 워크플로를 설명합니다.
HeartRateRecord 또는 OxygenSaturationRecord과 같은 데이터 유형을 수면 세션과 정렬하려면 세션의 startTime와 endTime 사이에 있는 타임스탬프를 사용하여 기록하세요. 헬스 커넥트는 세션 식별자를 사용하여 수면 세션을 세부 데이터와 연결하지 않습니다. 대신 연결은 겹치는 시간 간격을 통해 암시적으로 이루어집니다. 수면 데이터를 읽을 때 세션의 시간 범위를 사용하여 연결된 데이터 유형을 쿼리할 수 있습니다(수면 데이터 읽기 참고).
세션 작성
심박수와 같은 세부 데이터는 수면 세션 전체에 걸쳐 기록할 수 있지만 SleepSessionRecord 자체는 세션이 종료된 후에만 헬스 커넥트에 써야 합니다(예: 사용자가 잠에서 깬 경우). 레코드에는 세션 startTime, endTime, 세션 중에 기록된 SleepSessionRecord.Stage 객체 목록이 포함되어야 합니다. SleepSessionRecord에는 startTime 뒤에 endTime가 있어야 하기 때문입니다.
수면 세션을 작성하려면 다음 단계를 따르세요.
- 고유한 고객 레코드 ID를 생성합니다.
- 사용자가 일어나거나 수면 추적이 중지되면 모든 수면 단계를 수집하고
SleepSessionRecord를 구성합니다. insertRecords를 사용하여 레코드를 삽입합니다.
예:
val clientRecordId = UUID.randomUUID().toString()
val sessionStartTime = LocalDateTime.of(2023, 10, 30, 22, 0).toInstant(ZoneOffset.UTC)
val sessionEndTime = LocalDateTime.of(2023, 10, 31, 7, 0).toInstant(ZoneOffset.UTC)
val stages = mutableListOf<SleepSessionRecord.Stage>()
// Add recorded stages, for example:
stages.add(SleepSessionRecord.Stage(
startTime = sessionStartTime.plusSeconds(3600),
endTime = sessionStartTime.plusSeconds(7200),
stage = SleepSessionRecord.STAGE_TYPE_LIGHT)
)
stages.add(SleepSessionRecord.Stage(
startTime = sessionStartTime.plusSeconds(7200),
endTime = sessionStartTime.plusSeconds(10800),
stage = SleepSessionRecord.STAGE_TYPE_DEEP)
)
// ... other stages
val session = SleepSessionRecord(
startTime = sessionStartTime,
startZoneOffset = ZoneOffset.UTC,
endTime = sessionEndTime,
endZoneOffset = ZoneOffset.UTC,
stages = stages,
metadata = Metadata(clientRecordId = clientRecordId)
)
healthConnectClient.insertRecords(listOf(session))
수면 데이터 읽기
앱은 수면 세션과 연결된 데이터를 읽어 활동을 요약하거나, 건강 통계를 제공하거나, 외부 서버와 데이터를 동기화할 수 있습니다. 예를 들어 SleepSessionRecord를 읽은 다음 동일한 시간 간격 동안 발생한 HeartRateRecord를 쿼리할 수 있습니다.
연결된 데이터가 있는 읽기 세션
SleepSessionRecord을 레코드 유형으로 사용하여 ReadRecordsRequest을 통해 수면 세션을 읽을 수 있으며, 기간별로 필터링할 수 있습니다. 특정 세션의 연결된 데이터를 읽으려면 선택한 데이터 유형(예: HeartRateRecord)에 대해 두 번째 요청을 합니다. 수면 세션의 startTime 및 endTime로 필터링합니다.
다음 예에서는 지정된 시간 범위의 심박수 데이터와 연결된 수면 세션을 읽는 방법을 보여줍니다.
suspend fun readSleepSessionsWithAssociatedData(
healthConnectClient: HealthConnectClient,
startTime: Instant,
endTime: Instant
) {
val response = healthConnectClient.readRecords(
ReadRecordsRequest(
recordType = SleepSessionRecord::class,
timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
)
)
for (sleepRecord in response.records) {
// Process each session
val stages = sleepRecord.stages
val notes = sleepRecord.notes
// To read specific granular data (like heart rate) that occurred during
// this session, use the session's startTime and endTime to filter
// the request for that data type.
val hrResponse = healthConnectClient.readRecords(
ReadRecordsRequest(
recordType = HeartRateRecord::class,
timeRangeFilter = TimeRangeFilter.between(
sleepRecord.startTime,
sleepRecord.endTime
)
)
)
for (heartRateRecord in hrResponse.records) {
for (sample in heartRateRecord.samples) {
val bpm = sample.beatsPerMinute
}
}
}
}
권장사항
데이터 신뢰성과 사용자 환경을 개선하려면 다음 가이드라인을 따르세요.
- 쓰기 빈도
- 활성 추적(포그라운드): 활성 수면 추적의 경우 데이터가 제공되는 대로 또는 최대 15분 간격으로 데이터를 작성합니다.
- 백그라운드 동기화: 지연된 쓰기에는
WorkManager를 사용합니다. 실시간 데이터와 배터리 효율성 간의 균형을 맞추려면 15분 간격으로 설정하세요. - 일괄 처리: 모든 단일 센서 이벤트를 개별적으로 작성하지 마세요. 요청을 청크로 나눕니다. 헬스 커넥트는 쓰기 요청당 최대 1,000개의 레코드를 처리합니다.
- 세션 ID를 안정적이고 고유하게 유지: 세션에 일관된 식별자를 사용합니다. 세션을 수정하거나 업데이트할 때 동일한 ID를 사용하면 새 수면 세션으로 별도로 처리되지 않습니다.
- 데이터 유형에 일괄 처리 사용: 입력/출력 오버헤드를 줄이고 배터리 수명을 보존하려면 각 포인트를 개별적으로 쓰는 대신 데이터 포인트를 단일
insertRecords호출로 그룹화하세요. 중복 데이터 쓰기 방지: 클라이언트 ID 사용 레코드를 만들 때
metadata.clientRecordId를 설정합니다. 헬스 커넥트는 이를 사용하여 고유한 기록을 식별합니다. 이미 존재하는clientRecordId로 레코드를 쓰려고 하면 헬스 커넥트에서 중복을 무시하거나 새 레코드를 만드는 대신 기존 레코드를 업데이트합니다.metadata.clientRecordId를 설정하는 것이 동기화 재시도 또는 앱 재설치 중에 중복을 방지하는 가장 효과적인 방법입니다.val record = RespiratoryRateRecord( rate = 16.0, time = time, zoneOffset = ZoneOffset.UTC, metadata = Metadata( // Use a unique ID from your own database clientRecordId = "respiratory_rate_20231030_1" ) )기존 데이터 확인: 동기화하기 전에 시간 범위를 쿼리하여 앱의 레코드가 이미 있는지 확인합니다.
타임스탬프가 중복되지 않도록 함: 이전 세션이 종료되기 전에 새 세션이 시작되지 않는지 확인합니다. 세션이 중복되면 피트니스 대시보드와 요약 계산에 충돌이 발생할 수 있습니다.
권한에 대한 명확한 근거 제공:
Permission.createIntent흐름을 사용하여 앱에 '수면 패턴 분석'과 같은 건강 데이터에 대한 액세스가 필요한 이유를 설명합니다.장기 실행 세션 테스트: 여러 시간 동안 지속되는 세션 중에 배터리 소모를 모니터링하여 일괄 처리 간격과 센서 사용이 기기를 소모하지 않는지 확인합니다.
타임스탬프를 센서 속도와 정렬: 데이터 충실도를 유지하려면 기록 타임스탬프를 센서의 실제 빈도와 일치시킵니다.
테스트
데이터 정확성과 고품질 사용자 환경을 확인하려면 다음 테스트 전략을 따르고 공식 주요 사용 사례 테스트 문서를 참고하세요.
인증 도구
- 헬스 커넥트 도구 상자: 이 컴패니언 앱을 사용하여 레코드를 수동으로 검사하고, 테스트 데이터를 삭제하고, 데이터베이스의 변경사항을 시뮬레이션합니다. 레코드가 올바르게 저장되고 있는지 확인하는 가장 좋은 방법입니다.
FakeHealthConnectClient를 사용한 단위 테스트: 테스트 라이브러리를 사용하여 실제 기기 없이 권한 취소나 API 예외와 같은 극단적인 사례를 앱에서 처리하는 방식을 확인합니다.
품질 체크리스트
일반적인 아키텍처
수면 추적 구현에는 일반적으로 다음이 포함됩니다.
| 구성요소 | 관리 |
|---|---|
| 세션 컨트롤러 | 세션 상태 타이머 일괄 처리 로직 데이터 유형 컨트롤러 데이터 수집 |
| 저장소 레이어 (헬스 커넥트 작업을 래핑함) | 세션 삽입 데이터 유형 삽입 수면 단계 삽입 세션 요약 읽기 |
| UI 레이어 (디스플레이): | 지속 시간 실시간 데이터 유형 수면 단계 시각화 |
문제 해결
| 증상 | 가능한 원인 | 해상도 |
|---|---|---|
| 데이터 유형 누락 (예: 심박수) | 쓰기 권한이 누락되었거나 시간 필터가 잘못되었습니다. | 특정 데이터 유형 권한을 요청했고 사용자가 이를 부여했는지 확인합니다. ReadRecordsRequest이 세션과 일치하는 TimeRangeFilter을 사용하는지 확인합니다. 권한을 참고하세요. |
| 세션 쓰기 실패 | 타임스탬프가 중복됩니다. | 헬스 커넥트에서 동일한 앱의 기존 데이터와 중복되는 레코드를 거부할 수 있습니다. 새 세션의 startTime이 이전 세션의 endTime보다 늦은지 확인하세요. |
| 수면 중에 기록된 센서 데이터가 없음 | 포그라운드 서비스가 종료되었거나 비활성 상태입니다. | 화면이 꺼져 있는 동안 밤새 센서 데이터를 수집하려면 foregroundServiceType="health"와 함께 포그라운드 서비스를 사용하면 됩니다. |
| 중복 레코드가 표시됨 | clientRecordId 누락 |
각 레코드의 Metadata에 고유한 clientRecordId를 할당합니다. 이렇게 하면 동기화 재시도 중에 동일한 데이터가 두 번 쓰여진 경우 헬스 커넥트에서 중복 삭제를 실행할 수 있습니다. 권장사항을 참고하세요. |
일반적인 디버깅 단계
- 권한 상태 확인: 읽기 또는 쓰기 작업을 시도하기 전에 항상
getPermissionStatus()를 호출합니다. 사용자는 언제든지 시스템 설정에서 권한을 취소할 수 있습니다. - 실행 모드 확인: 앱이 백그라운드에서 데이터를 수집하지 않는 경우
AndroidManifest.xml에서 올바른 권한을 선언했는지, 사용자가 앱을 '배터리 제한' 모드로 설정하지 않았는지 확인합니다.