使用「健康資料同步」開發睡眠體驗

如要在應用程式中建構睡眠追蹤體驗,可以使用健康資料同步執行下列操作:

  • 寫入睡眠時段
  • 寫入睡眠階段資料
  • 寫入睡眠資料,例如心率、血氧濃度和呼吸速率
  • 讀取其他應用程式的睡眠資料

本指南說明如何建構這些睡眠功能,涵蓋資料類型、背景執行、權限、建議的工作流程和最佳做法。

總覽:建構完整的睡眠追蹤器

如要使用「健康資料同步」建構完整的睡眠追蹤體驗,請按照下列核心步驟操作:

  • 根據健康資料存取權正確實作權限。
  • 使用 SleepSessionRecord 錄製工作階段。
  • 在工作階段期間,持續寫入睡眠階段、心率和血氧濃度等資料類型。
  • 妥善管理背景執行作業,以便驗證夜間是否持續擷取資料。
  • 讀取睡眠後摘要和分析的睡眠階段資料。

這個工作流程可與其他「健康資料同步」應用程式互通,並驗證使用者控管的資料存取權。

事前準備

實作睡眠功能前,請先瞭解以下事項:

核心概念

「健康資料同步」會使用幾個核心元件來表示睡眠資料。SleepSessionRecord 是睡眠的中央記錄,包含開始或結束時間和睡眠階段等詳細資料。在工作階段期間,系統會記錄各種資料類型,例如 HeartRateRecordOxygenSaturationRecord

睡眠記錄

睡眠資料以 SleepSessionRecord 表示。每筆記錄會儲存:

  • startTime
  • endTime
  • stagesSleepSessionRecord.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 要求這些權限。系統接著會顯示 Health Connect 權限畫面。

// 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)
  }
}

由於使用者可以隨時授予或撤銷權限,應用程式每次使用權限前都必須先檢查,並處理權限遭撤銷的情況。

導入睡眠時段

本節說明記錄睡眠資料的建議工作流程。

如要將 HeartRateRecordOxygenSaturationRecord 等資料類型與睡眠時段對齊,請使用介於時段 startTimeendTime 之間的時間戳記記錄這些資料。「健康資料同步」不會使用工作階段 ID,將睡眠工作階段與詳細資料連結。而是透過重疊的時間間隔隱含關聯。讀取睡眠資料時,您可以透過工作階段的時間範圍查詢相關聯的資料類型,如「讀取睡眠資料」一文所示。

寫入活動

雖然系統可以在睡眠期間記錄心率等詳細資料,但只有在睡眠結束後 (例如使用者醒來時),才能將 SleepSessionRecord 寫入「健康資料同步」。記錄必須包含工作階段 startTimeendTime,以及工作階段期間記錄的 SleepSessionRecord.Stage 物件清單,因為 SleepSessionRecord 要求 endTime 必須在 startTime 之後。

如要寫入睡眠時段資料,請按照下列步驟操作:

  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

讀取工作階段和相關聯的資料

您可以使用 ReadRecordsRequest 讀取睡眠階段,並以 SleepSessionRecord 做為記錄類型,然後依時間範圍篩選。如要讀取特定睡眠階段的相關資料,請對所選資料類型發出第二個要求,例如依 startTimeendTime 篩選睡眠階段。HeartRateRecordstartTimeendTime

以下範例說明如何讀取特定時間範圍內的睡眠時段,以及相關聯的心率資料:

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 分鐘,在即時資料和電池效率之間取得平衡。
    • 批次處理:請勿個別寫入每個感應器事件。將要求分塊。每個寫入要求最多可處理 1000 筆記錄。
  • 確保工作階段 ID 穩定且不重複:為工作階段使用一致的 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 流程說明應用程式需要存取健康資料的原因,例如「分析睡眠模式」。

  • 測試長時間工作階段:監控數小時工作階段的電池耗電量,確認批次間隔和感應器用量不會耗盡裝置電力。

  • 將時間戳記與感應器速率對齊:將記錄時間戳記與感應器的實際頻率相符,以維持高保真度的資料。

測試

如要驗證資料正確性並確保使用者享有優質體驗,請依循下列測試策略,並參閱官方的測試熱門用途說明文件。

驗證工具

品質檢查清單

一般架構

睡眠追蹤功能通常包含以下項目:

元件 管理
工作階段控制器 工作階段狀態
計時器
批次處理邏輯
資料類型控制器
資料收集
存放區層 (包裝健康資料同步作業): 插入睡眠階段
插入資料類型
插入睡眠階段
讀取睡眠階段摘要
UI 層 (顯示): 睡眠時間長度
即時資料類型
睡眠階段視覺化

疑難排解

問題 可能原因 解析度
缺少資料類型 (例如心率) 缺少寫入權限或時間篩選器有誤。 確認您已要求特定資料類型權限,且使用者已授予該權限。確認 ReadRecordsRequest 使用的 TimeRangeFilter 與工作階段相符。請參閱「權限」。
工作階段無法寫入 時間戳記重疊。 如果記錄與來自同一應用程式的現有資料重疊,健康資料同步可能會拒絕。請確認新活動的 startTime晚於前一活動的 endTime
睡眠期間未記錄感應器資料 前景服務已終止或無效。 如要在螢幕關閉時收集整夜的感應器資料,可以使用搭配 foregroundServiceType="health"前景服務
出現重複記錄 缺少 clientRecordId 在每筆記錄的 Metadata 中指派專屬的 clientRecordId。這樣一來,如果同步重試期間寫入相同資料兩次,健康資料同步就能執行重複資料刪除作業。請參閱「最佳做法」。

常見的偵錯步驟

  • 檢查權限狀態:嘗試讀取或寫入作業前,請務必先呼叫 getPermissionStatus()。使用者隨時可以在系統設定中撤銷權限。
  • 驗證執行模式:如果應用程式未在背景收集資料,請確認您已在 AndroidManifest.xml 中聲明正確的權限,且使用者未將應用程式設為「電池用量限制」模式。