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