Introdução à PHR nas APIs do framework do Android

Para usar as APIs do framework do Android para registros de saúde pessoais (PHRs), você precisa:

  1. Configurar o ambiente usando o Android Studio
  2. Instanciar HealthConnectManager
  3. Processar permissões
  4. Criar origens de dados

Configurar o ambiente usando o Android Studio

Para ter acesso às APIs mais recentes, você também precisa ter acesso às ferramentas mais recentes:

  • Instale a versão mais recente do Android Studio.
  • No Android Studio, acesse Tools > SDK Manager.
    • Na guia "SDK Platforms", selecione Android Baklava.
    • Esta versão inclui o nível 36 da API e as APIs PHR.
    • Na guia "SDK Tools", selecione as Android SDK Build-Tools mais recentes disponíveis.
    • Clique em "OK" para instalar o SDK.
  • Use a versão mais recente disponível do Plug-in do Android para Gradle.

Declarar níveis de API

Para acessar as APIs de PHR, você precisa segmentar a versão adequada do Android. Isso é feito declarando os níveis da API em app/build.gradle.

...
compileSdk = 36

defaultConfig {
  targetSdk = 36
...

Saiba mais sobre como configurar os níveis de API na documentação uses-sdk-element e Configurar o SDK do Android 16.

Declarar permissões

Adicionar novas permissões de PHR em AndroidManifest.xml. Declare permissões apenas para os tipos de dados que você pretende usar no aplicativo. A lista completa de permissões relacionadas a informações médicas é mostrada no bloco de código. As permissões não relacionadas à PHR para a Conexão Saúde não estão incluídas.

...
  <!--  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"/>
...

Se um aplicativo tentar usar uma API que exige uma permissão e essa permissão não for declarada no manifesto do app, uma SecurityException será gerada.

Para uma exceção do tipo SecurityException, uma mensagem de erro útil estará disponível em exception.localizedMessage.

Criar uma instância do HealthConnectManager

HealthConnectManager é a classe responsável por processar interações de permissões, bem como leituras e gravações no repositório de dados local da Conexão Saúde. Vamos conhecer os métodos de instância de HealthConnectManager nas próximas seções. Como o HealthConnectManager expõe um serviço do sistema, não é possível instanciar essa classe diretamente. Use getSystemService. Em particular, observe que o serviço do sistema está intimamente vinculado ao contexto em que é instanciado e não pode ser acessível fora desse contexto.

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

Processar permissões

O usuário do seu app precisa conceder a permissão para acessar os dados do app Conexão Saúde. Para fazer isso, inicie uma nova atividade com as permissões especificadas e consuma a lista resultante de permissões concedidas. A interface da nova atividade permite que o usuário selecione as permissões necessárias para conceder ao app. Solicite permissões apenas para os tipos de dados que você pretende que o aplicativo use.

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)

Tentar ler ou gravar sem uma permissão necessária vai resultar em uma HealthConnectException.

Para uma exceção do tipo HealthConnectException, uma mensagem de erro útil estará disponível em exception.localizedMessage.

Criar origens de dados

Para gravar dados de saúde na Conexão Saúde, seu app precisa primeiro criar uma fonte de dados para armazenar as informações. Uma fonte de dados geralmente representa uma API ou um sistema médico específico.

No exemplo, criamos uma fonte de dados chamada My Hospital e especificamos a versão do 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"),
)

Gravar registros

Prepare exemplos de registros FHIR em JSON. Há várias fontes na Web com exemplos de dados no formato 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"
      }
    }
  ]
}

Insira os dados:

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 usa como argumento uma lista de UpsertMedicalResourceRequest. Se qualquer UpsertMedicalResourceRequest individual transmitido na chamada não for inserido, nenhuma gravação será confirmada no repositório para a lista inteira de UpsertMedicalResourceRequest.

Se qualquer solicitação tiver IDs MedicalDataSource inválidos, a API vai gerar um IllegalArgumentException. Se uma solicitação for considerada inválida por qualquer outro motivo, o autor da chamada vai receber um HealthConnectException.

A chave exclusiva de uma solicitação específica é a combinação do ID da fonte de dados, do tipo de recurso FHIR e do ID do recurso FHIR. Se esses três itens em uma solicitação corresponderem a um registro existente, uma atualização será acionada. Caso contrário, um novo registro será criado.

Ler registros

Leia os registros por tipo e processe os resultados como achar melhor.

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
}