Làm quen với PHR trên các API khung Android

Để sử dụng các API khung Android cho Hồ sơ sức khoẻ cá nhân (PHR), bạn phải:

  1. Định cấu hình môi trường bằng Android Studio
  2. Tạo thực thể cho HealthConnectManager
  3. Xử lý quyền
  4. Tạo nguồn dữ liệu

Định cấu hình môi trường bằng Android Studio

Để có quyền truy cập vào các API mới nhất, bạn cũng cần có quyền truy cập vào các công cụ liên quan mới nhất:

  • Cài đặt phiên bản Android Studio mới nhất.
  • Trong Android Studio, hãy chuyển đến mục Công cụ > Trình quản lý SDK.
    • Trong thẻ Nền tảng SDK, hãy chọn Android Baklava.
    • Phiên bản này bao gồm API cấp 36 và API PHR.
    • Trong thẻ Bộ công cụ SDK, hãy chọn Android SDK Build-Tools (Bộ công cụ bản dựng SDK Android) mới nhất hiện có.
    • Nhấp vào OK để cài đặt SDK.
  • Sử dụng phiên bản mới nhất hiện có của trình bổ trợ Android cho Gradle.

Khai báo cấp độ API

Để truy cập vào các API PHR, bạn cần nhắm đến phiên bản Android phù hợp. Bạn có thể thực hiện việc này bằng cách khai báo các cấp độ API trong app/build.gradle.

...
compileSdk = 36

defaultConfig {
  targetSdk = 36
...

Tìm hiểu thêm về cách định cấu hình cấp độ API trong phần tử uses-sdk và tài liệu Thiết lập SDK Android 16.

Khai báo quyền

Thêm các quyền mới về hồ sơ sức khoẻ cá nhân (PHR) trong AndroidManifest.xml. Chỉ khai báo quyền cho những loại dữ liệu mà bạn dự định sử dụng trong ứng dụng. Danh sách đầy đủ các quyền liên quan đến y tế được hiển thị trong khối mã. Không bao gồm các quyền không liên quan đến PHR cho 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"/>
...

Nếu một ứng dụng cố gắng thực thi một API yêu cầu quyền và quyền đó không được khai báo trong tệp kê khai của ứng dụng, thì SecurityException sẽ được gửi.

Đối với trường hợp ngoại lệ thuộc loại SecurityException, một thông báo lỗi hữu ích sẽ có trong exception.localizedMessage.

Tạo bản sao HealthConnectManager

HealthConnectManager là lớp chịu trách nhiệm xử lý các hoạt động tương tác về quyền cũng như đọc và ghi vào kho lưu trữ dữ liệu cục bộ của Health Connect. Chúng ta sẽ khám phá các phương thức thực thể của HealthConnectManager trong các phần tiếp theo. Vì HealthConnectManager hiển thị một dịch vụ hệ thống, nên bạn không thể tạo bản sao trực tiếp của lớp này và phải sử dụng getSystemService. Cụ thể, hãy lưu ý rằng dịch vụ hệ thống được ghép nối chặt chẽ với ngữ cảnh mà dịch vụ đó được tạo thực thể và không được truy cập được bên ngoài ngữ cảnh đó.

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

Xử lý quyền

Người dùng ứng dụng của bạn phải cấp cho ứng dụng quyền truy cập vào dữ liệu Health Connect. Để thực hiện việc này, hãy khởi chạy một hoạt động mới với các quyền được chỉ định và sử dụng danh sách quyền đã cấp. Giao diện người dùng của hoạt động mới sẽ cho phép người dùng chọn các quyền họ cần cấp cho ứng dụng của bạn. Chỉ yêu cầu quyền đối với những loại dữ liệu mà bạn dự định ứng dụng của mình sẽ sử dụng.

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)

Việc cố gắng đọc hoặc ghi mà không có quyền bắt buộc sẽ dẫn đến HealthConnectException.

Đối với trường hợp ngoại lệ thuộc loại HealthConnectException, một thông báo lỗi hữu ích sẽ có trong exception.localizedMessage.

Tạo nguồn dữ liệu

Để ghi dữ liệu sức khoẻ vào Health Connect, trước tiên, ứng dụng của bạn phải tạo một nguồn dữ liệu để lưu trữ thông tin. Nguồn dữ liệu thường đại diện cho một API hoặc hệ thống y tế cụ thể.

Trong ví dụ này, chúng ta tạo một nguồn dữ liệu có tên là My Hospital và chỉ định phiên bản 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"),
)

Ghi bản ghi

Chuẩn bị các bản ghi FHIR mẫu ở định dạng JSON. Có nhiều nguồn trên web cung cấp dữ liệu mẫu ở định dạng 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"
      }
    }
  ]
}

Chèn dữ liệu:

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)

upsertMedicalResources lấy danh sách UpsertMedicalResourceRequest làm đối số. Nếu không chèn được bất kỳ UpsertMedicalResourceRequest cá nhân nào được truyền vào lệnh gọi, thì sẽ không có hoạt động ghi nào được thực hiện đối với toàn bộ danh sách UpsertMedicalResourceRequest.

Nếu có yêu cầu nào chứa mã MedicalDataSource không hợp lệ, API sẽ gửi một IllegalArgumentException. Nếu bất kỳ yêu cầu nào được coi là không hợp lệ vì bất kỳ lý do nào khác, thì phương thức gọi sẽ nhận được HealthConnectException.

Khoá duy nhất cho một yêu cầu cụ thể là tổ hợp của mã nguồn dữ liệu, loại tài nguyên FHIR và mã tài nguyên FHIR. Nếu 3 mục này trong một yêu cầu khớp với một bản ghi hiện có, thì hệ thống sẽ kích hoạt một bản cập nhật. Nếu không, một bản ghi mới sẽ được tạo.

Đọc bản ghi

Đọc bản ghi theo loại, sau đó xử lý kết quả theo ý bạn.

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
}