开始使用 Android 框架 API 中的 PHR

如需针对个人健康记录 (PHR) 使用 Android 框架 API,您必须满足以下条件:

  1. 使用 Android Studio 配置环境
  2. 实例化 HealthConnectManager
  3. 处理权限
  4. 创建数据源

使用 Android Studio 配置环境

如需访问最新的 API,您还需要访问最新的相关工具:

  • 安装最新版本的 Android Studio
  • 在 Android Studio 中,依次前往 Tools > SDK 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
...

如需详细了解如何配置 API 级别,请参阅 uses-sdk-element设置 Android 16 SDK 文档。

声明权限

AndroidManifest.xml 中添加了新的个人健康记录权限。 请仅为您打算在应用中使用的那些数据类型声明权限。代码块中列出了与医疗相关的权限的完整列表。不包括 Health Connect 的非个人健康记录相关权限。

...
  <!--  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 是负责处理权限互动以及对 Health Connect 本地数据库的读写操作的类。我们将在后续部分中介绍 HealthConnectManager 的实例方法。由于 HealthConnectManager 会公开系统服务,因此您无法直接实例化此类,而必须使用 getSystemService。特别要注意的是,系统服务与其实例化的上下文紧密耦合,不应在该上下文之外提供访问权限。

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

处理权限

应用的用户必须向您的应用授予访问 Health Connect 数据的权限。为此,请启动具有指定权限的新 activity,并使用授予的权限的生成列表。用户可以在新 activity 的界面中选择需要向您的应用授予的权限。请仅针对您的应用打算使用的那些数据类型请求权限。

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 中会显示一条有用的错误消息。

创建数据源

如需将健康数据写入 Health Connect,您的应用必须先创建一个数据源来存储信息。数据源通常表示特定的 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
}