헬스 커넥트로 수면 환경 개발

앱에서 수면 추적 환경을 빌드하려는 경우 헬스 커넥트를 사용하여 다음과 같은 작업을 할 수 있습니다.

  • 수면 세션 쓰기
  • 수면 단계 데이터 쓰기
  • 심박수, 산소 포화도, 호흡수와 같은 수면 데이터 쓰기
  • 다른 앱의 수면 데이터 읽기

이 가이드에서는 데이터 유형, 백그라운드 실행, 권한, 권장 워크플로, 권장사항을 다루면서 이러한 수면 기능을 빌드하는 방법을 설명합니다.

개요: 포괄적인 수면 추적기 빌드

다음과 같은 핵심 단계를 따라 헬스 커넥트를 사용하여 포괄적인 수면 추적 환경을 빌드할 수 있습니다.

  • 건강 권한에 따라 권한을 올바르게 구현합니다.
  • SleepSessionRecord를 사용하여 세션을 기록합니다.
  • 세션 중에 수면 단계, 심박수, 산소 포화도와 같은 데이터 유형을 일관되게 씁니다.
  • 백그라운드 실행을 올바르게 관리하여 밤새도록 지속적인 데이터 캡처를 확인합니다.
  • 수면 후 요약 및 분석을 위해 세션 데이터를 읽습니다.

이 워크플로를 사용하면 다른 헬스 커넥트 앱과의 상호 운용이 가능하고 사용자 제어 데이터 액세스를 확인할 수 있습니다.

시작하기 전에

수면 기능을 구현하기 전에 다음을 실행합니다.

주요 개념

헬스 커넥트는 몇 가지 핵심 구성요소를 사용하여 수면 데이터를 나타냅니다. SleepSessionRecord는 수면의 중앙 레코드 역할을 하며 시작 또는 종료 시간, 수면 단계와 같은 세부정보를 포함합니다. 세션 중에 HeartRateRecord 또는 OxygenSaturationRecord와 같은 다양한 데이터 유형을 기록할 수 있습니다.

수면 세션

수면 데이터는 SleepSessionRecord로 표시됩니다. 각 레코드는 다음을 저장합니다.

  • startTime
  • endTime
  • stages: 깊은 수면, 얕은 수면, 렘수면, 깨어 있는 수면을 포함한 SleepSessionRecord.Stage 목록입니다.
  • 선택적 세션 메타데이터 (제목, 메모)

앱은 세션과 연결된 여러 데이터 유형을 쓸 수 있습니다.

데이터 유형

수면 세션 중에 기록되는 일반적인 데이터 유형은 다음과 같습니다.

각 데이터 유형은 개별 레코드로 저장됩니다.

개발 고려 사항

수면 추적 앱은 화면이 꺼져 있을 때 백그라운드에서 자주 실행되므로 장시간 실행해야 하는 경우가 많습니다. 수면 기능을 빌드할 때는 백그라운드 실행을 관리하고 수면 데이터에 필요한 권한을 요청하는 방법을 고려하는 것이 중요합니다.

백그라운드 실행

수면 추적 앱은 일반적으로 화면이 꺼진 상태로 밤새도록 실행됩니다. 이 상태에서는 다음을 사용해야 합니다.

  • 데이터 수집을 위한 포그라운드 서비스
  • 지연된 쓰기 또는 동기화를 위한 WorkManager
  • 심박수와 같은 세분화된 데이터의 정기적인 레코드 쓰기를 위한 일괄 처리 전략

모든 쓰기에서 세션 ID를 일관되게 유지하여 연속성을 유지합니다.

권한

앱은 수면 데이터를 읽거나 쓰기 전에 관련 헬스 커넥트 권한을 요청해야 합니다. 데이터 유형의 전체 목록은 헬스 커넥트 데이터 유형을 참고하세요. 수면의 일반적인 권한에는 수면 세션과 심박수 또는 산소 포화도와 같은 측정항목이 포함됩니다.

수면 액세스는 다음 권한으로 보호됩니다.

  • android.permission.health.READ_SLEEP
  • android.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와 같은 데이터 유형을 수면 세션과 정렬하려면 세션의 startTimeendTime 사이에 있는 타임스탬프를 사용하여 기록합니다. 헬스 커넥트는 세션 식별자를 사용하여 수면 세션을 세분화된 데이터와 연결하지 않습니다. 대신 겹치는 시간 간격을 통해 연결이 암시적으로 이루어집니다. 수면 데이터를 읽을 때는 세션의 시간 범위를 사용하여 수면 데이터 읽기 에 표시된 대로 연결된 데이터 유형을 쿼리할 수 있습니다.

세션 쓰기

심박수와 같은 세분화된 데이터는 수면 세션 내내 기록할 수 있지만 SleepSessionRecord 자체는 세션이 완료된 후에만 헬스 커넥트에 써야 합니다(예: 사용자가 깨어났을 때). SleepSessionRecordendTimestartTime 이후여야 하므로 레코드에는 세션 startTime, endTime, 세션 중에 기록된 SleepSessionRecord.Stage 객체 목록이 포함되어야 합니다.

수면 세션을 쓰려면 다음 단계를 따르세요.

  1. 고유한 클라이언트 레코드 ID를 생성합니다.
  2. 사용자가 깨어나거나 수면 추적이 중지되면 모든 수면 단계를 수집하고 SleepSessionRecord를 구성합니다.
  3. 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를 사용하여 수면 세션을 읽을 수 있습니다. 특정 세션의 연결된 데이터를 읽으려면 수면 세션의 startTimeendTime으로 필터링하여 HeartRateRecord와 같은 선택한 데이터 유형에 대한 두 번째 요청을 합니다.

다음 예는 지정된 시간 범위의 연결된 심박수 데이터로 수면 세션을 읽는 방법을 보여줍니다.

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 사용: 지연된 쓰기에 WorkManager를 사용합니다. 실시간 데이터와 배터리 효율성 간의 균형을 맞추려면 15분 간격을 목표로 합니다.
  • 일괄 처리 쓰기 요청: 모든 단일 센서 이벤트를 개별적으로 쓰지 마세요. 요청을 청크합니다. 헬스 커넥트는 쓰기 요청당 최대 1,000개의 레코드를 처리합니다.
  • 세션 ID를 안정적이고 고유하게 유지: 세션에 일관된 식별자를 사용합니다. 세션을 수정하거나 업데이트하는 경우 동일한 ID를 사용하면 새롭고 별도의 세션으로 처리되지 않습니다.
  • 데이터 유형에 일괄 처리 사용: 입/출력 오버헤드를 줄이고 배터리 수명을 보존하려면 각 포인트를 개별적으로 쓰는 대신 데이터 포인트를 단일 insertRecords 호출로 그룹화합니다.
  • 중복 데이터 쓰기 방지: 클라이언트 ID 사용: 레코드를 만들 때 metadata.clientRecordId를 설정합니다. 헬스 커넥트는 이를 사용하여 고유한 레코드를 식별합니다. 이미 존재하는 clientRecordId로 레코드를 쓰려고 하면 헬스 커넥트는 새 레코드를 만드는 대신 중복을 무시하거나 기존 레코드를 업데이트합니다. metadata.clientRecordId를 설정하는 것이 동기화 재시도 또는 앱 재설치 중에 중복을 방지하는 가장 효과적인 방법입니다.
    val record = StepsRecord(
        count = 100,
        startTime = startTime,
        endTime = endTime,
        startZoneOffset = ZoneOffset.UTC,
        endZoneOffset = ZoneOffset.UTC,
        metadata = Metadata(
            // Use a unique ID from your own database
            clientRecordId = "daily_steps_2023_10_27_user_123"
        )
    )
  • 기존 데이터 확인: 동기화하기 전에 시간 범위를 쿼리하여 앱의 레코드가 이미 존재하는지 확인합니다.
  • 타임스탬프가 겹치지 않도록 하기: 새 세션이 이전 세션이 끝나기 전에 시작되지 않는지 확인합니다. 세션이 겹치면 피트니스 대시보드 및 요약 계산에 충돌이 발생할 수 있습니다.
  • 권한에 대한 명확한 근거 제공: Permission.createIntent 흐름을 사용하여 앱이 건강 데이터에 액세스해야 하는 이유를 설명합니다(예: '혈압 추세를 모니터링하고 통계를 제공하기 위해').
  • 장기 실행 세션 테스트: 여러 시간 동안 지속되는 세션 중에 배터리 소모를 모니터링하여 일괄 처리 간격과 센서 사용량이 기기를 소모하지 않는지 확인합니다.
  • 타임스탬프를 센서 속도에 맞게 정렬: 데이터 충실도를 유지하려면 레코드 타임스탬프를 센서의 실제 빈도와 일치시킵니다.

테스트

데이터 정확성과 고품질 사용자 환경을 확인하려면 다음 테스트 전략을 따르고 공식 테스트 주요 사용 사례 문서를 참고하세요.

확인 도구

  • 헬스 커넥트 도구 상자: 이 컴패니언 앱을 사용하여 레코드를 수동으로 검사하고, 테스트 데이터를 삭제하고, 데이터베이스 변경사항을 시뮬레이션합니다. 레코드가 올바르게 저장되고 있는지 확인하는 가장 좋은 방법입니다.
  • FakeHealthConnectClient를 사용한 단위 테스트: 테스트 라이브러리 를 사용하여 실제 기기 없이 권한 취소 또는 API 예외와 같은 앱의 엣지 케이스 처리 방법을 확인합니다.

품질 체크리스트

일반적인 아키텍처

수면 추적 구현에는 일반적으로 다음이 포함됩니다.

구성요소 관리
세션 컨트롤러 세션 상태
타이머
일괄 처리 로직
데이터 유형 컨트롤러
데이터 수집
저장소 레이어 (헬스 커넥트 작업 래핑) 세션 삽입
데이터 유형 삽입
수면 단계 삽입
세션 요약 읽기
UI 레이어 (디스플레이): 기간
실시간 데이터 유형
수면 단계 시각화

문제 해결

증상 가능한 원인 해결 방법
데이터 유형 누락 (예: 심박수) 쓰기 권한 누락 또는 잘못된 시간 필터입니다. 특정 데이터 유형 권한을 요청했고 사용자가 부여했는지 확인합니다. ReadRecordsRequest가 세션과 일치하는 TimeRangeFilter를 사용하는지 확인합니다. 권한을 참고하세요.
세션을 쓰지 못함 타임스탬프가 겹칩니다. 헬스 커넥트는 동일한 앱의 기존 데이터와 겹치는 레코드를 거부할 수 있습니다. 새 세션의 startTime이 이전 세션의 endTime 이후인지 확인합니다.
수면 중에 기록된 센서 데이터가 없음 포그라운드 서비스가 종료되었거나 비활성 상태입니다. 화면이 꺼진 상태로 밤새도록 센서 데이터를 수집하려면 포그라운드 서비스를 사용하여 foregroundServiceType="health"
중복 레코드가 표시됨 clientRecordId가 누락되었습니다. 각 레코드의 Metadata에 고유한 clientRecordId를 할당합니다. 이렇게 하면 동기화 재시도 중에 동일한 데이터가 두 번 쓰여지는 경우 헬스 커넥트에서 중복 삭제를 실행할 수 있습니다. 권장사항을 참고하세요.

일반적인 디버깅 단계

권한 상태를 확인합니다. 읽기 또는 쓰기 작업을 시도하기 전에 항상 getPermissionStatus()를 호출합니다. 사용자는 언제든지 시스템 설정에서 권한을 취소할 수 있습니다.
실행 모드를 확인합니다. 앱이 백그라운드에서 데이터를 수집하지 않는 경우 AndroidManifest.xml 파일에 올바른 권한을 선언했고 사용자가 앱을 '배터리 제한' 모드로 설정하지 않았는지 확인합니다.