ヘルスコネクトで睡眠エクスペリエンスを開発する

このガイドは、ヘルスコネクトのバージョン 1.1.0 以降に対応しています。

アプリで睡眠管理エクスペリエンスを構築する場合は、ヘルスコネクトを使用して次のようなことができます。

  • 睡眠セッションの書き込み
  • 睡眠ステージのデータを書き込む
  • 心拍数、酸素飽和度、呼吸数などの睡眠データの書き込み
  • 他のアプリから睡眠データを読み取る

このガイドでは、データタイプ、バックグラウンド実行、権限、推奨されるワークフロー、ベスト プラクティスなど、これらの睡眠機能を構築する方法について説明します。

概要: 包括的な睡眠トラッカーの構築

ヘルスコネクトを使用して包括的な睡眠管理エクスペリエンスを構築するには、次の主要な手順に沿って操作します。

  • Health に関する権限に基づいて権限を正しく実装している。
  • 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 マニフェストで宣言されていることを確認します。

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 を使用して権限をリクエストします。ヘルスコネクトの権限画面が表示されます。
val permissions = setOf(
        HealthPermission.getReadPermission(StepsRecord::class),
        HealthPermission.getWritePermission(StepsRecord::class),
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getWritePermission(HeartRateRecord::class)
    )

val requestPermissionsLauncher = rememberLauncherForActivityResult(
    contract = PermissionController.createRequestPermissionResultContract()
) { grantedPermissions ->
    if (grantedPermissions.containsAll(permissions)) {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions granted!") }
    } else {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions denied.") }
    }
}
ユーザーはいつでも権限を付与または取り消すことができるため、アプリは権限を使用する前に毎回権限をチェックし、権限が失われた状況に対応できるように設計する必要があります。

睡眠セッションを実装する

このセクションでは、睡眠データを記録するための推奨ワークフローについて説明します。

HeartRateRecord や OxygenSaturationRecord などのデータタイプを睡眠セッションに合わせるには、セッションの startTime と endTime の間のタイムスタンプで記録します。ヘルスコネクトでは、セッション識別子を使用して睡眠セッションを詳細データにリンクすることはありません。代わりに、重複する時間間隔によって関連付けが暗黙的に行われます。睡眠データを読み取る場合は、睡眠データの読み取りに示すように、セッションの期間を使用して、関連するデータ型をクエリできます。

セッションを書き込む

心拍数などの詳細なデータは睡眠セッション全体で記録できますが、SleepSessionRecord 自体は、セッションが終了したとき(ユーザーが目覚めたときなど)にのみヘルスコネクトに書き込む必要があります。SleepSessionRecord では endTime が startTime より後であることが求められるため、レコードにはセッションの 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 をクエリできます。

関連データを含む読み取りセッション

睡眠セッションを読み取るには、ReadRecordsRequest を使用します。このとき、レコードタイプとして SleepSessionRecord を指定し、期間でフィルタします。特定のセッションに関連付けられたデータを読み取るには、選択したデータタイプ(HeartRateRecord など)に対して 2 回目のリクエストを行い、睡眠セッションの 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 を使用する: 遅延書き込みには WorkManager を使用します。リアルタイム データとバッテリー効率のバランスを取るため、15 分間隔を目指します。
  • 書き込みリクエストをバッチ処理する: センサー イベントを個別に書き込まないでください。リクエストをチャンク化します。ヘルスコネクトは、1 回の書き込みリクエストにつき最大 1,000 件のレコードを処理します。
  • セッション ID を安定した一意のものにする: セッションには一貫した識別子を使用します。セッションが編集または更新された場合、同じ ID を使用することで、新しい別のセッションとして扱われるのを防ぐことができます。
  • データ型にバッチ処理を使用する: 入出力のオーバーヘッドを削減し、バッテリー駆動時間を延ばすには、各データポイントを個別に書き込むのではなく、1 つの 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 を割り当てます。これにより、同期の再試行中に同じデータが 2 回書き込まれた場合に、ヘルスコネクトが重複除去を実行できるようになります。ベスト プラクティスをご覧ください。

一般的なデバッグ手順

権限の状態を確認します。 読み取りまたは書き込みオペレーションを試行する前に、必ず getPermissionStatus() を呼び出してください。ユーザーはいつでもシステム設定で権限を取り消すことができます。
実行モードを確認します。 アプリがバックグラウンドでデータを収集していない場合は、AndroidManifest.xml ファイルで正しい権限を宣言していること、およびユーザーがアプリを「バッテリー制限」モードにしていないことを確認してください。