1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.healthconnect.testapps.toolbox.ui 18 19 import android.content.Context 20 import android.health.connect.CreateMedicalDataSourceRequest 21 import android.health.connect.HealthConnectManager 22 import android.health.connect.MedicalIdFilter 23 import android.health.connect.datatypes.MedicalDataSource 24 import android.health.connect.datatypes.MedicalResource 25 import android.os.Bundle 26 import android.util.Log 27 import android.view.View 28 import android.widget.Button 29 import android.widget.EditText 30 import android.widget.Toast 31 import androidx.activity.result.ActivityResultLauncher 32 import androidx.activity.result.contract.ActivityResultContracts 33 import androidx.appcompat.app.AlertDialog 34 import androidx.core.os.asOutcomeReceiver 35 import androidx.fragment.app.Fragment 36 import androidx.lifecycle.lifecycleScope 37 import com.android.healthconnect.testapps.toolbox.Constants.MEDICAL_PERMISSIONS 38 import com.android.healthconnect.testapps.toolbox.Constants.READ_IMMUNIZATION 39 import com.android.healthconnect.testapps.toolbox.R 40 import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils.Companion.requireSystemService 41 import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils.Companion.showMessageDialog 42 import java.io.IOException 43 import kotlinx.coroutines.launch 44 import kotlinx.coroutines.suspendCancellableCoroutine 45 46 class PhrOptionsFragment : Fragment(R.layout.fragment_phr_options) { 47 48 private lateinit var mRequestPermissionLauncher: ActivityResultLauncher<Array<String>> 49 private val healthConnectManager: HealthConnectManager by lazy { 50 requireContext().requireSystemService() 51 } 52 53 override fun onCreate(savedInstanceState: Bundle?) { 54 super.onCreate(savedInstanceState) 55 56 // Starting API Level 30 If permission is denied more than once, user doesn't see the dialog 57 // asking permissions again unless they grant the permission from settings. 58 mRequestPermissionLauncher = 59 registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { 60 permissionMap: Map<String, Boolean> -> 61 requestPermissionResultHandler(permissionMap) 62 } 63 } 64 65 private fun requestPermissionResultHandler(permissionMap: Map<String, Boolean>) { 66 var numberOfPermissionsMissing = MEDICAL_PERMISSIONS.size 67 for (value in permissionMap.values) { 68 if (value) { 69 numberOfPermissionsMissing-- 70 } 71 } 72 73 if (numberOfPermissionsMissing == 0) { 74 Toast.makeText( 75 this.requireContext(), 76 R.string.all_medical_permissions_success, 77 Toast.LENGTH_SHORT) 78 .show() 79 } else { 80 Toast.makeText( 81 this.requireContext(), 82 getString( 83 R.string.number_of_medical_permissions_not_granted, 84 numberOfPermissionsMissing), 85 Toast.LENGTH_SHORT) 86 .show() 87 } 88 } 89 90 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 91 super.onViewCreated(view, savedInstanceState) 92 93 view.requireViewById<Button>(R.id.phr_create_data_source_button).setOnClickListener { 94 executeAndShowMessage { 95 createMedicalDataSource("Hospital X", "example.fhir.com/R4/123") 96 } 97 } 98 99 view.requireViewById<Button>(R.id.phr_insert_immunization_button).setOnClickListener { 100 executeAndShowMessage { insertImmunization(view) } 101 } 102 103 view.requireViewById<Button>(R.id.phr_read_by_id_button).setOnClickListener { 104 executeAndShowMessage { readMedicalResourceForIdFromTextbox(view) } 105 } 106 107 view.requireViewById<Button>(R.id.phr_seed_fhir_jsons_button).setOnClickListener { 108 loadAllFhirJSONs() 109 } 110 111 view.requireViewById<Button>(R.id.phr_request_read_immunization_button).setOnClickListener { 112 requestReadImmunizationPermission() 113 } 114 115 view 116 .requireViewById<Button>(R.id.phr_request_read_and_write_medical_data_button) 117 .setOnClickListener { requestMedicalPermissions() } 118 } 119 120 private fun executeAndShowMessage(block: suspend () -> String) { 121 lifecycleScope.launch { 122 val result = 123 try { 124 block() 125 } catch (e: Exception) { 126 e.toString() 127 } 128 129 requireContext().showMessageDialog(result) 130 } 131 } 132 133 private suspend fun insertImmunization(view: View): String { 134 val immunizationResource = loadJSONFromAsset(requireContext(), "immunization_1.json") 135 Log.d("INSERT_MEDICAL_RESOURCE", "Writing immunization ${immunizationResource}") 136 // TODO(b/343375877) Replace this with call to HC after insert API is implemented 137 val insertedResourceId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" 138 view.findViewById<EditText>(R.id.phr_immunization_id_text).setText(insertedResourceId) 139 return insertedResourceId 140 } 141 142 private suspend fun createMedicalDataSource(displayName: String, fhirBaseUri: String): String { 143 val dataSource = 144 suspendCancellableCoroutine<MedicalDataSource> { continuation -> 145 healthConnectManager.createMedicalDataSource( 146 CreateMedicalDataSourceRequest.Builder(displayName, fhirBaseUri).build(), 147 Runnable::run, 148 continuation.asOutcomeReceiver()) 149 } 150 Log.d("CREATE_MEDICAL_DATA_SOURCE", "Created source: ${dataSource.toString()}") 151 return dataSource.toString() 152 } 153 154 private suspend fun readMedicalResourceForIdFromTextbox(view: View): String { 155 val resourceId = 156 view.findViewById<EditText>(R.id.phr_immunization_id_text).getText().toString() 157 return readMedicalResourcesById(listOf(resourceId)) 158 .joinToString(separator = "\n", transform = MedicalResource::toString) 159 } 160 161 private suspend fun readMedicalResourcesById(ids: List<String>): List<MedicalResource> { 162 Log.d("READ_MEDICAL_RESOURCES", "Reading resource with ids ${ids.toString()}") 163 val resources = 164 suspendCancellableCoroutine<List<MedicalResource>> { continuation -> 165 healthConnectManager.readMedicalResources( 166 ids.map(MedicalIdFilter::fromId), 167 Runnable::run, 168 continuation.asOutcomeReceiver()) 169 } 170 Log.d("READ_MEDICAL_RESOURCES", "Read ${resources.size} resources") 171 return resources 172 } 173 174 private fun loadAllFhirJSONs() { 175 val jsonFiles = listFhirJSONFiles(requireContext()) 176 if (jsonFiles == null) { 177 Log.e("loadAllFhirJSONs", "No JSON files were found.") 178 Toast.makeText(context, "No JSON files were found.", Toast.LENGTH_SHORT).show() 179 return 180 } 181 182 for (jsonFile in jsonFiles) { 183 if (!jsonFile.endsWith(".json")) { 184 continue 185 } 186 val jsonString = loadJSONFromAsset(requireContext(), jsonFile) 187 if (jsonString != null) { 188 Log.i("loadAllFhirJSONs", "$jsonFile: $jsonString") 189 } 190 } 191 showLoadJSONDataDialog() 192 } 193 194 private fun listFhirJSONFiles(context: Context, path: String = ""): List<String>? { 195 val assetManager = context.assets 196 return try { 197 assetManager.list(path)?.toList() ?: emptyList() 198 } catch (e: IOException) { 199 Log.e("listFhirJSONFiles", "Error listing assets in path $path: $e") 200 Toast.makeText( 201 context, "Error listing JSON files: ${e.localizedMessage}", Toast.LENGTH_SHORT) 202 .show() 203 null 204 } 205 } 206 207 fun loadJSONFromAsset(context: Context, fileName: String): String? { 208 return try { 209 val inputStream = context.assets.open(fileName) 210 val buffer = ByteArray(inputStream.available()) 211 inputStream.read(buffer) 212 inputStream.close() 213 buffer.toString(Charsets.UTF_8) 214 } catch (e: IOException) { 215 Log.e("loadJSONFromAsset", "Error reading JSON file: $e") 216 Toast.makeText( 217 context, "Error reading JSON file: ${e.localizedMessage}", Toast.LENGTH_SHORT) 218 .show() 219 null 220 } 221 } 222 223 private fun showLoadJSONDataDialog() { 224 val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext()) 225 builder.setTitle("FHIR JSON files loaded.") 226 builder.setPositiveButton(android.R.string.ok) { _, _ -> } 227 val alertDialog: AlertDialog = builder.create() 228 alertDialog.show() 229 } 230 231 private fun requestReadImmunizationPermission() { 232 mRequestPermissionLauncher.launch(arrayOf(READ_IMMUNIZATION)) 233 } 234 235 private fun requestMedicalPermissions() { 236 mRequestPermissionLauncher.launch(MEDICAL_PERMISSIONS) 237 } 238 } 239