Android 프레임워크 API에서 PHR 시작하기

개인 건강 기록 (PHR)에 Android 프레임워크 API를 사용하려면 다음을 실행해야 합니다.

  1. Android 스튜디오를 사용하여 환경 구성
  2. HealthConnectManager 인스턴스화
  3. 권한 처리
  4. 데이터 소스 만들기

Android 스튜디오를 사용하여 환경 구성

최신 API에 액세스하려면 최신 관련 도구에도 액세스해야 합니다.

  • Android 스튜디오의 최신 버전을 설치합니다.
  • Android 스튜디오에서 Tools > AVD Manager로 이동합니다.
    • SDK Platforms 탭에서 Android Baklava를 선택합니다.
    • 이 버전에는 API 수준 36 및 PHR API가 포함되어 있습니다.
    • SDK Tools 탭에서 사용 가능한 최신 Android SDK Build-Tools를 선택합니다.
    • '확인'을 클릭하여 SDK를 설치합니다.
  • 사용 가능한 최신 버전의 Android Gradle 플러그인을 사용하세요.

API 수준 선언

PHR API에 액세스하려면 적절한 버전의 Android를 타겟팅해야 합니다. app/build.gradle에서 API 수준을 선언하면 됩니다.

...
compileSdk = 36

defaultConfig {
  targetSdk = 36
...

uses-sdk-elementAndroid 16 SDK 설정 문서에서 API 수준 구성에 관해 자세히 알아보세요.

권한 선언

AndroidManifest.xml에 새로운 PHR 권한을 추가합니다. 애플리케이션에서 사용할 데이터 유형에 대해서만 권한을 선언합니다. 의료 관련 권한의 전체 목록은 코드 블록에 표시됩니다. 헬스 커넥트의 PHR 이외 권한은 포함되지 않습니다.

...
  <!--  Medical permissions -->
  <uses-permission android:name="android.permission.health.WRITE_MEDICAL_DATA"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_ALLERGIES_INTOLERANCES"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_CONDITIONS"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_IMMUNIZATIONS"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_LABORATORY_RESULTS"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_MEDICATIONS"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_PERSONAL_DETAILS"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_PRACTITIONER_DETAILS"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_PREGNANCY"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_PROCEDURES"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_SOCIAL_HISTORY"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_VISITS"/>
  <uses-permission android:name="android.permission.health.READ_MEDICAL_DATA_VITAL_SIGNS"/>
...

애플리케이션이 권한이 필요한 API를 실행하려고 하는데 이 권한이 앱의 매니페스트에 선언되지 않은 경우 SecurityException이 발생합니다.

SecurityException 유형의 예외의 경우 exception.localizedMessage에서 유용한 오류 메시지를 사용할 수 있습니다.

HealthConnectManager 인스턴스화

HealthConnectManager는 권한 상호작용과 헬스 커넥트 로컬 데이터 저장소에 대한 읽기 및 쓰기를 처리하는 클래스입니다. 다음 섹션에서는 HealthConnectManager의 인스턴스 메서드를 살펴봅니다. HealthConnectManager는 시스템 서비스를 노출하므로 이 클래스를 직접 인스턴스화할 수 없으며 getSystemService를 사용해야 합니다. 특히 시스템 서비스는 인스턴스화되는 컨텍스트와 밀접하게 결합되며 해당 컨텍스트 외부에서 액세스할 수 없어야 합니다.

import android.health.connect.HealthConnectManager
...
val healthConnectManager: HealthConnectManager = requireNotNull(applicationContext.getSystemService(HealthConnectManager::class.java))

권한 처리

애플리케이션의 사용자는 애플리케이션에 헬스 커넥트 데이터에 액세스할 권한을 부여해야 합니다. 이렇게 하려면 권한이 지정된 새 활동을 실행하고 부여된 권한 목록을 사용합니다. 새 활동의 UI를 통해 사용자는 앱에 부여해야 하는 권한을 선택할 수 있습니다. 애플리케이션에서 사용할 데이터 유형에 대해서만 권한을 요청하세요.

val MEDICAL_PERMISSIONS = arrayOf(
            "android.permission.health.READ_MEDICAL_DATA_ALLERGIES_INTOLERANCES",
            "android.permission.health.READ_MEDICAL_DATA_CONDITIONS",
            "android.permission.health.READ_MEDICAL_DATA_IMMUNIZATIONS",
            "android.permission.health.READ_MEDICAL_DATA_LABORATORY_RESULTS",
            "android.permission.health.READ_MEDICAL_DATA_MEDICATIONS",
            "android.permission.health.READ_MEDICAL_DATA_PERSONAL_DETAILS",
            "android.permission.health.READ_MEDICAL_DATA_PRACTITIONER_DETAILS",
            "android.permission.health.READ_MEDICAL_DATA_PREGNANCY",
            "android.permission.health.READ_MEDICAL_DATA_PROCEDURES",
            "android.permission.health.READ_MEDICAL_DATA_SOCIAL_HISTORY",
            "android.permission.health.READ_MEDICAL_DATA_VISITS",
            "android.permission.health.READ_MEDICAL_DATA_VITAL_SIGNS",
            "android.permission.health.WRITE_MEDICAL_DATA",
        )
...
private lateinit var mRequestPermissionLauncher: ActivityResultLauncher<Array<String>>
...
mRequestPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
            permissionMap: Map<String, Boolean> ->
            requestPermissionResultHandler(permissionMap)
        }
}
...
private fun requestPermissionResultHandler(permissionMap: Map<String, Boolean>) {
    // Evaluate permissionMap and handle any missing permissions
}
...
mRequestPermissionLauncher.launch(MEDICAL_PERMISSIONS)

필요한 권한 없이 읽거나 쓰려고 하면 HealthConnectException이 발생합니다.

HealthConnectException 유형의 예외의 경우 exception.localizedMessage에서 유용한 오류 메시지를 사용할 수 있습니다.

데이터 소스 만들기

헬스 커넥트에 건강 데이터를 쓰려면 먼저 애플리케이션에서 정보를 보관할 데이터 소스를 만들어야 합니다. 데이터 소스는 일반적으로 특정 API 또는 의료 시스템을 나타냅니다.

이 예에서는 My Hospital라는 데이터 소스를 만들고 FHIR 버전을 지정합니다.

import android.health.connect.CreateMedicalDataSourceRequest
import android.health.connect.HealthConnectManager
import android.health.connect.datatypes.FhirVersion
import android.health.connect.datatypes.MedicalDataSource
import android.net.Uri
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.os.asOutcomeReceiver
import kotlinx.coroutines.suspendCancellableCoroutine
...
private suspend fun createMedicalDataSource(
    fhirBaseUri: Uri,
    displayName: String,
    fhirVersion: FhirVersion,
): String {
    val dataSource =
        suspendCancellableCoroutine<MedicalDataSource> { continuation ->
            healthConnectManager.createMedicalDataSource(
                CreateMedicalDataSourceRequest.Builder(fhirBaseUri,
                                                       displayName,
                                                       fhirVersion).build(),
                Runnable::run,
                continuation.asOutcomeReceiver(),
            )
        }
    Log.d("CREATE_DATA_SOURCE", "Created source: $dataSource")
    return "Created data source: $displayName"
}
...
createMedicalDataSource(
    Uri.parse("example.fhir.com/R4/123"),
    "My Hospital",
    FhirVersion.parseFhirVersion("4.0.1"),
)

레코드 작성

JSON에서 FHIR 레코드 예시를 준비합니다. 웹에는 FHIR 형식의 예시 데이터가 있는 다양한 소스가 있습니다.

{
  "resourceType": "Immunization",
  "id": "immunization-1",
  "status": "completed",
  "vaccineCode": {
    "coding": [
      {
        "system": "http://hl7.org/fhir/sid/cvx",
        "code": "115"
      },
      {
        "system": "http://hl7.org/fhir/sid/ndc",
        "code": "58160-842-11"
      }
    ],
    "text": "Tdap"
  },
  "patient": {
    "reference": "Patient/patient_1",
    "display": "Example, Anne"
  },
  "encounter": {
    "reference": "Encounter/encounter_unk",
    "display": "GP Visit"
  },
  "occurrenceDateTime": "2018-05-21",
  "primarySource": true,
  "manufacturer": {
    "display": "Sanofi Pasteur"
  },
  "lotNumber": "1",
  "site": {
    "coding": [
      {
        "system": "http://terminology.hl7.org/CodeSystem/v3-ActSite",
        "code": "LA",
        "display": "Left Arm"
      }
    ],
    "text": "Left Arm"
  },
  "route": {
    "coding": [
      {
        "system": "http://terminology.hl7.org/CodeSystem/v3-RouteOfAdministration",
        "code": "IM",
        "display": "Injection, intramuscular"
      }
    ],
    "text": "Injection, intramuscular"
  },
  "doseQuantity": {
    "value": 0.5,
    "unit": "mL"
  },
  "performer": [
    {
      "function": {
        "coding": [
          {
            "system": "http://terminology.hl7.org/CodeSystem/v2-0443",
            "code": "AP",
            "display": "Administering Provider"
          }
        ],
        "text": "Administering Provider"
      },
      "actor": {
        "reference": "Practitioner/practitioner_1",
        "type": "Practitioner",
        "display": "Dr Maria Hernandez"
      }
    }
  ]
}

데이터를 삽입합니다.

import android.health.connect.UpsertMedicalResourceRequest
import android.health.connect.datatypes.MedicalResource
...
private suspend fun loadJSONFromAsset(assetName: String): String {
...
private suspend fun upsertMedicalResources(
    requests: List<UpsertMedicalResourceRequest>
): List<MedicalResource> {
    Log.d("UPSERT_RESOURCES", "Writing ${requests.size} resources")
    val resources =
        suspendCancellableCoroutine<List<MedicalResource>> { continuation ->
            healthConnectManager.upsertMedicalResources(
                requests,
                Runnable::run,
                continuation.asOutcomeReceiver(),
            )
        }
    Log.d("UPSERT_RESOURCES", "Wrote ${resources.size} resources")
    return resources
}
...
private suspend fun insertResource(insertedDataSourceId: String, resource: String): String {
    val insertedResources =
        upsertMedicalResources(
            listOf(
                UpsertMedicalResourceRequest.Builder(
                        insertedDataSourceId,
                        FhirVersion.parseFhirVersion("4.0.1"),
                        resource,
                    )
                    .build()
            )
        )
    return insertedResources.joinToString(
        separator = "\n",
        transform = MedicalResource::toString,
    )
}
...
val immunizationResource =
    loadJSONFromAsset("immunization_1.json")
insertResource(dataSource.id, immunizationResource)

upsertMedicalResourcesUpsertMedicalResourceRequest 목록을 인수로 사용합니다. 호출에 전달된 개별 UpsertMedicalResourceRequest가 삽입되지 않으면 전체 UpsertMedicalResourceRequest 목록에 대한 쓰기가 저장소에 커밋되지 않습니다.

요청에 잘못된 MedicalDataSource ID가 포함된 경우 API에서 IllegalArgumentException을 발생시킵니다. 다른 이유로 요청이 유효하지 않은 것으로 간주되면 호출자에게 HealthConnectException이 수신됩니다.

특정 요청의 고유 키는 데이터 소스 ID, FHIR 리소스 유형, FHIR 리소스 ID의 조합입니다. 요청의 세 항목이 기존 레코드와 일치하면 업데이트가 트리거됩니다. 그렇지 않으면 새 레코드가 생성됩니다.

레코드 읽기

유형별로 레코드를 읽은 다음 적절하다고 생각하는 대로 결과를 처리합니다.

import android.health.connect.ReadMedicalResourcesInitialRequest
import android.health.connect.ReadMedicalResourcesResponse
import android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_IMMUNIZATIONS
...
private suspend fun readImmunization(): List<MedicalResource> {

    var receiver: OutcomeReceiver<ReadMedicalResourcesResponse, HealthConnectException>
    val request: ReadMedicalResourcesInitialRequest =
        ReadMedicalResourcesInitialRequest.Builder(MEDICAL_RESOURCE_TYPE_IMMUNIZATIONS).build()
    val resources =
        suspendCancellableCoroutine<ReadMedicalResourcesResponse> { continuation ->
                receiver = continuation.asOutcomeReceiver()
                healthConnectManager.readMedicalResources(request, Runnable::run, receiver)
            }
            .medicalResources
    Log.d("READ_MEDICAL_RESOURCES", "Read ${resources.size} resources")
    return resources
}