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