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.HealthConnectException 22 import android.health.connect.HealthConnectManager 23 import android.health.connect.ReadMedicalResourcesInitialRequest 24 import android.health.connect.ReadMedicalResourcesResponse 25 import android.health.connect.UpsertMedicalResourceRequest 26 import android.health.connect.datatypes.FhirVersion 27 import android.health.connect.datatypes.MedicalDataSource 28 import android.health.connect.datatypes.MedicalResource 29 import android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_VACCINES 30 import android.net.Uri 31 import android.os.Bundle 32 import android.os.OutcomeReceiver 33 import android.util.Log 34 import android.view.View 35 import android.widget.AdapterView 36 import android.widget.ArrayAdapter 37 import android.widget.Button 38 import android.widget.EditText 39 import android.widget.Spinner 40 import android.widget.Toast 41 import androidx.activity.result.ActivityResultLauncher 42 import androidx.activity.result.contract.ActivityResultContracts 43 import androidx.core.os.asOutcomeReceiver 44 import androidx.fragment.app.Fragment 45 import androidx.lifecycle.lifecycleScope 46 import com.android.healthconnect.testapps.toolbox.Constants.MEDICAL_PERMISSIONS 47 import com.android.healthconnect.testapps.toolbox.R 48 import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils.Companion.requireSystemService 49 import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils.Companion.showMessageDialog 50 import java.io.IOException 51 import kotlinx.coroutines.launch 52 import kotlinx.coroutines.suspendCancellableCoroutine 53 54 class PhrOptionsFragment : Fragment(R.layout.fragment_phr_options) { 55 56 private lateinit var mRequestPermissionLauncher: ActivityResultLauncher<Array<String>> 57 private val healthConnectManager: HealthConnectManager by lazy { 58 requireContext().requireSystemService() 59 } 60 61 override fun onCreate(savedInstanceState: Bundle?) { 62 super.onCreate(savedInstanceState) 63 64 // Starting API Level 30 If permission is denied more than once, user doesn't see the dialog 65 // asking permissions again unless they grant the permission from settings. 66 mRequestPermissionLauncher = 67 registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { 68 permissionMap: Map<String, Boolean> -> 69 requestPermissionResultHandler(permissionMap) 70 } 71 } 72 73 private fun requestPermissionResultHandler(permissionMap: Map<String, Boolean>) { 74 var numberOfPermissionsMissing = MEDICAL_PERMISSIONS.size 75 for (value in permissionMap.values) { 76 if (value) { 77 numberOfPermissionsMissing-- 78 } 79 } 80 81 if (numberOfPermissionsMissing == 0) { 82 Toast.makeText( 83 this.requireContext(), 84 R.string.all_medical_permissions_success, 85 Toast.LENGTH_SHORT, 86 ) 87 .show() 88 } else { 89 Toast.makeText( 90 this.requireContext(), 91 getString( 92 R.string.number_of_medical_permissions_not_granted, 93 numberOfPermissionsMissing, 94 ), 95 Toast.LENGTH_SHORT, 96 ) 97 .show() 98 } 99 } 100 101 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 102 super.onViewCreated(view, savedInstanceState) 103 104 view.requireViewById<Button>(R.id.phr_create_data_source_button).setOnClickListener { 105 executeAndShowMessage { 106 createMedicalDataSource( 107 view, 108 Uri.parse("example.fhir.com/R4/123"), 109 "My Hospital " + (0..1000).random(), 110 FhirVersion.parseFhirVersion("4.0.1"), 111 ) 112 } 113 } 114 115 view.requireViewById<Button>(R.id.phr_read_by_id_button).setOnClickListener { 116 executeAndShowMessage { readImmunization(view) } 117 } 118 119 view.requireViewById<Button>(R.id.phr_seed_fhir_jsons_button).setOnClickListener { 120 executeAndShowMessage { insertAllFhirResources(view) } 121 } 122 123 view.requireViewById<Button>(R.id.phr_insert_immunization_button).setOnClickListener { 124 executeAndShowMessage { insertImmunization(view) } 125 } 126 127 view.requireViewById<Button>(R.id.phr_insert_allergy_button).setOnClickListener { 128 executeAndShowMessage { insertAllergy(view) } 129 } 130 131 view 132 .requireViewById<Button>(R.id.phr_request_read_and_write_medical_data_button) 133 .setOnClickListener { requestMedicalPermissions() } 134 135 setUpFhirResourceFromSpinner(view) 136 } 137 138 private fun executeAndShowMessage(block: suspend () -> String) { 139 lifecycleScope.launch { 140 val result = 141 try { 142 block() 143 } catch (e: Exception) { 144 e.toString() 145 } 146 147 requireContext().showMessageDialog(result) 148 } 149 } 150 151 private suspend fun insertImmunization(view: View): String { 152 val immunizationResource = 153 loadJSONFromAsset(requireContext(), "immunization_1.json") 154 ?: return "No Immunization resource to insert" 155 Log.d("INSERT_IMMUNIZATION", "Writing immunization $immunizationResource") 156 return insertResource(view, immunizationResource) 157 } 158 159 private suspend fun insertAllergy(view: View): String { 160 val allergyResource = 161 loadJSONFromAsset(requireContext(), "allergyintolerance_1.json") 162 ?: return "No Allergy resource to insert" 163 Log.d("INSERT_ALLERGY", "Writing allergy $allergyResource") 164 return insertResource(view, allergyResource) 165 } 166 167 private fun setUpFhirResourceFromSpinner(rootView: View) { 168 val jsonFiles = getJsonFilesFromAssets(requireContext()) 169 val spinnerOptions = listOf(getString(R.string.spinner_default_message)) + jsonFiles 170 val spinnerAdapter = 171 ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, spinnerOptions) 172 spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) 173 174 val spinner = rootView.findViewById<Spinner>(R.id.phr_spinner) 175 spinner.adapter = spinnerAdapter 176 177 spinner.onItemSelectedListener = 178 object : AdapterView.OnItemSelectedListener { 179 override fun onItemSelected( 180 parent: AdapterView<*>, 181 view: View, 182 position: Int, 183 id: Long, 184 ) { 185 if (position == 0) { // Ignore "Select resource" default message 186 return 187 } 188 189 val selectedFile = spinnerOptions[position] 190 val selectedResource = 191 loadJSONFromAsset(requireContext(), selectedFile) ?: return 192 Log.d("INSERT_RESOURCE_FROM_SPINNER", "Writing resource $selectedResource") 193 executeAndShowMessage { insertResource(rootView, selectedResource) } 194 } 195 196 override fun onNothingSelected(parent: AdapterView<*>) { 197 // No-op. 198 } 199 } 200 } 201 202 private suspend fun insertResource(view: View, resource: String): String { 203 val insertedDataSourceId = 204 view.findViewById<EditText>(R.id.phr_data_source_id_text).getText().toString() 205 val insertedResources = 206 upsertMedicalResources( 207 listOf( 208 UpsertMedicalResourceRequest.Builder( 209 insertedDataSourceId, 210 FhirVersion.parseFhirVersion("4.0.1"), 211 resource, 212 ) 213 .build() 214 ) 215 ) 216 return insertedResources.joinToString( 217 separator = "\n", 218 transform = MedicalResource::toString, 219 ) 220 } 221 222 private suspend fun insertAllFhirResources(view: View): String { 223 val allResources = loadAllFhirJSONs() 224 Log.d("INSERT_ALL", "Writing all FHIR resources") 225 val insertedDataSourceId = 226 view.findViewById<EditText>(R.id.phr_data_source_id_text).getText().toString() 227 val insertedResources = 228 upsertMedicalResources( 229 allResources.map { 230 UpsertMedicalResourceRequest.Builder( 231 insertedDataSourceId, 232 FhirVersion.parseFhirVersion("4.0.1"), 233 it, 234 ) 235 .build() 236 } 237 ) 238 return "SUCCESSFUL DATA UPSERT \n\nUpserted data:\n" + 239 insertedResources.joinToString(separator = "\n", transform = MedicalResource::toString) 240 } 241 242 private fun getJsonFilesFromAssets(context: Context): List<String> { 243 return context.assets.list("")?.filter { it.endsWith(".json") } ?: emptyList() 244 } 245 246 private suspend fun upsertMedicalResources( 247 requests: List<UpsertMedicalResourceRequest> 248 ): List<MedicalResource> { 249 Log.d("UPSERT_RESOURCES", "Writing ${requests.size} resources") 250 val resources = 251 suspendCancellableCoroutine<List<MedicalResource>> { continuation -> 252 healthConnectManager.upsertMedicalResources( 253 requests, 254 Runnable::run, 255 continuation.asOutcomeReceiver(), 256 ) 257 } 258 Log.d("UPSERT_RESOURCES", "Wrote ${resources.size} resources") 259 return resources 260 } 261 262 private suspend fun createMedicalDataSource( 263 view: View, 264 fhirBaseUri: Uri, 265 displayName: String, 266 fhirVersion: FhirVersion, 267 ): String { 268 val dataSource = 269 suspendCancellableCoroutine<MedicalDataSource> { continuation -> 270 healthConnectManager.createMedicalDataSource( 271 CreateMedicalDataSourceRequest.Builder(fhirBaseUri, displayName, fhirVersion) 272 .build(), 273 Runnable::run, 274 continuation.asOutcomeReceiver(), 275 ) 276 } 277 view.findViewById<EditText>(R.id.phr_data_source_id_text).setText(dataSource.id) 278 Log.d("CREATE_DATA_SOURCE", "Created source: $dataSource") 279 return "Created data source: $displayName" 280 } 281 282 private suspend fun readImmunization(view: View): String { 283 return readImmunization() 284 .joinToString(separator = "\n", transform = MedicalResource::toString) 285 } 286 287 private suspend fun readImmunization(): List<MedicalResource> { 288 289 var receiver: OutcomeReceiver<ReadMedicalResourcesResponse, HealthConnectException> 290 val request: ReadMedicalResourcesInitialRequest = 291 ReadMedicalResourcesInitialRequest.Builder(MEDICAL_RESOURCE_TYPE_VACCINES).build() 292 val resources = 293 suspendCancellableCoroutine<ReadMedicalResourcesResponse> { continuation -> 294 receiver = continuation.asOutcomeReceiver() 295 healthConnectManager.readMedicalResources(request, Runnable::run, receiver) 296 } 297 .medicalResources 298 Log.d("READ_MEDICAL_RESOURCES", "Read ${resources.size} resources") 299 return resources 300 } 301 302 private fun loadAllFhirJSONs(): List<String> { 303 val jsonFiles = listFhirJSONFiles(requireContext()) 304 if (jsonFiles == null) { 305 Log.e("loadAllFhirJSONs", "No JSON files were found.") 306 Toast.makeText(context, "No JSON files were found.", Toast.LENGTH_SHORT).show() 307 return emptyList() 308 } 309 310 return jsonFiles 311 .filter { it.endsWith(".json") } 312 .mapNotNull { 313 val jsonString = loadJSONFromAsset(requireContext(), it) 314 Log.i("loadAllFhirJSONs", "$it: $jsonString") 315 jsonString 316 } 317 } 318 319 private fun listFhirJSONFiles(context: Context, path: String = ""): List<String>? { 320 val assetManager = context.assets 321 return try { 322 assetManager.list(path)?.toList() ?: emptyList() 323 } catch (e: IOException) { 324 Log.e("listFhirJSONFiles", "Error listing assets in path $path: $e") 325 Toast.makeText( 326 context, 327 "Error listing JSON files: ${e.localizedMessage}", 328 Toast.LENGTH_SHORT, 329 ) 330 .show() 331 null 332 } 333 } 334 335 fun loadJSONFromAsset(context: Context, fileName: String): String? { 336 return try { 337 val inputStream = context.assets.open(fileName) 338 val buffer = ByteArray(inputStream.available()) 339 inputStream.read(buffer) 340 inputStream.close() 341 buffer.toString(Charsets.UTF_8) 342 } catch (e: IOException) { 343 Log.e("loadJSONFromAsset", "Error reading JSON file: $e") 344 Toast.makeText( 345 context, 346 "Error reading JSON file: ${e.localizedMessage}", 347 Toast.LENGTH_SHORT, 348 ) 349 .show() 350 null 351 } 352 } 353 354 private fun requestMedicalPermissions() { 355 mRequestPermissionLauncher.launch(MEDICAL_PERMISSIONS) 356 } 357 } 358