1 /**
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * ```
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  * ```
10  *
11  * Unless required by applicable law or agreed to in writing, software distributed under the License
12  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13  * or implied. See the License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 package com.android.healthconnect.testapps.toolbox.ui
17 
18 import android.health.connect.HealthConnectManager
19 import android.health.connect.datatypes.BasalBodyTemperatureRecord
20 import android.health.connect.datatypes.BloodGlucoseRecord
21 import android.health.connect.datatypes.BloodPressureRecord
22 import android.health.connect.datatypes.BodyTemperatureMeasurementLocation
23 import android.health.connect.datatypes.BodyTemperatureRecord
24 import android.health.connect.datatypes.CervicalMucusRecord
25 import android.health.connect.datatypes.ExerciseRoute
26 import android.health.connect.datatypes.ExerciseSessionRecord
27 import android.health.connect.datatypes.ExerciseSessionType
28 import android.health.connect.datatypes.FloorsClimbedRecord
29 import android.health.connect.datatypes.InstantRecord
30 import android.health.connect.datatypes.IntervalRecord
31 import android.health.connect.datatypes.MealType
32 import android.health.connect.datatypes.MenstruationFlowRecord
33 import android.health.connect.datatypes.OvulationTestRecord
34 import android.health.connect.datatypes.PlannedExerciseSessionRecord
35 import android.health.connect.datatypes.Record
36 import android.health.connect.datatypes.SexualActivityRecord
37 import android.health.connect.datatypes.SkinTemperatureRecord
38 import android.health.connect.datatypes.Vo2MaxRecord
39 import android.health.connect.datatypes.units.BloodGlucose
40 import android.health.connect.datatypes.units.Energy
41 import android.health.connect.datatypes.units.Length
42 import android.health.connect.datatypes.units.Mass
43 import android.health.connect.datatypes.units.Percentage
44 import android.health.connect.datatypes.units.Power
45 import android.health.connect.datatypes.units.Pressure
46 import android.health.connect.datatypes.units.Temperature
47 import android.health.connect.datatypes.units.TemperatureDelta
48 import android.health.connect.datatypes.units.Volume
49 import android.os.Bundle
50 import android.util.Log
51 import android.view.LayoutInflater
52 import android.view.View
53 import android.view.ViewGroup
54 import android.widget.Button
55 import android.widget.LinearLayout
56 import android.widget.TextView
57 import android.widget.Toast
58 import androidx.appcompat.app.AlertDialog
59 import androidx.fragment.app.Fragment
60 import androidx.fragment.app.viewModels
61 import androidx.navigation.NavController
62 import androidx.navigation.fragment.findNavController
63 import com.android.healthconnect.testapps.toolbox.Constants.HealthPermissionType
64 import com.android.healthconnect.testapps.toolbox.Constants.INPUT_TYPE_DOUBLE
65 import com.android.healthconnect.testapps.toolbox.Constants.INPUT_TYPE_INT
66 import com.android.healthconnect.testapps.toolbox.Constants.INPUT_TYPE_LONG
67 import com.android.healthconnect.testapps.toolbox.Constants.INPUT_TYPE_TEXT
68 import com.android.healthconnect.testapps.toolbox.R
69 import com.android.healthconnect.testapps.toolbox.data.ExerciseRoutesTestData.Companion.routeDataMap
70 import com.android.healthconnect.testapps.toolbox.fieldviews.DateTimePicker
71 import com.android.healthconnect.testapps.toolbox.fieldviews.EditableTextView
72 import com.android.healthconnect.testapps.toolbox.fieldviews.EnumDropDown
73 import com.android.healthconnect.testapps.toolbox.fieldviews.InputFieldView
74 import com.android.healthconnect.testapps.toolbox.fieldviews.ListInputField
75 import com.android.healthconnect.testapps.toolbox.utils.EnumFieldsWithValues
76 import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils
77 import com.android.healthconnect.testapps.toolbox.utils.InsertOrUpdateRecords.Companion.createRecordObject
78 import com.android.healthconnect.testapps.toolbox.viewmodels.InsertOrUpdateRecordsViewModel
79 import java.lang.reflect.Field
80 import java.lang.reflect.ParameterizedType
81 import kotlin.reflect.KClass
82 
83 class InsertRecordFragment : Fragment() {
84 
85     private lateinit var mRecordFields: Array<Field>
86     private lateinit var mRecordClass: KClass<out Record>
87     private lateinit var mNavigationController: NavController
88     private lateinit var mFieldNameToFieldInput: HashMap<String, InputFieldView>
89     private lateinit var mLinearLayout: LinearLayout
90     private lateinit var mHealthConnectManager: HealthConnectManager
91     private lateinit var mUpdateRecordUuid: InputFieldView
92 
93     private val mInsertOrUpdateViewModel: InsertOrUpdateRecordsViewModel by viewModels()
94 
95     override fun onCreateView(
96         inflater: LayoutInflater,
97         container: ViewGroup?,
98         savedInstanceState: Bundle?,
99     ): View {
100         mInsertOrUpdateViewModel.insertedRecordsState.observe(viewLifecycleOwner) { state ->
101             when (state) {
102                 is InsertOrUpdateRecordsViewModel.InsertedRecordsState.WithData -> {
103                     showInsertSuccessDialog(state.entries)
104                 }
105                 is InsertOrUpdateRecordsViewModel.InsertedRecordsState.Error -> {
106                     Toast.makeText(
107                             context,
108                             "Unable to insert record(s)! ${state.errorMessage}",
109                             Toast.LENGTH_SHORT)
110                         .show()
111                 }
112             }
113         }
114 
115         mInsertOrUpdateViewModel.updatedRecordsState.observe(viewLifecycleOwner) { state ->
116             if (state is InsertOrUpdateRecordsViewModel.UpdatedRecordsState.Error) {
117                 Toast.makeText(
118                         context,
119                         "Unable to update record(s)! ${state.errorMessage}",
120                         Toast.LENGTH_SHORT)
121                     .show()
122             } else {
123                 Toast.makeText(context, "Successfully updated record(s)!", Toast.LENGTH_SHORT)
124                     .show()
125             }
126         }
127         return inflater.inflate(R.layout.fragment_insert_record, container, false)
128     }
129 
130     private fun showInsertSuccessDialog(records: List<Record>) {
131         val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
132         builder.setTitle("Record UUID(s)")
133         builder.setMessage(records.joinToString { it.metadata.id })
134         builder.setPositiveButton(android.R.string.ok) { _, _ -> }
135         val alertDialog: AlertDialog = builder.create()
136         alertDialog.show()
137         alertDialog.findViewById<TextView>(android.R.id.message)?.setTextIsSelectable(true)
138     }
139 
140     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
141         super.onViewCreated(view, savedInstanceState)
142         mNavigationController = findNavController()
143         mHealthConnectManager =
144             requireContext().getSystemService(HealthConnectManager::class.java)!!
145 
146         val permissionType =
147             arguments?.getSerializable("permissionType", HealthPermissionType::class.java)
148                 ?: throw java.lang.IllegalArgumentException("Please pass the permissionType.")
149 
150         mFieldNameToFieldInput = HashMap()
151         mRecordFields = permissionType.recordClass?.java?.declaredFields as Array<Field>
152         mRecordClass = permissionType.recordClass
153         view.findViewById<TextView>(R.id.title).setText(permissionType.title)
154         mLinearLayout = view.findViewById(R.id.record_input_linear_layout)
155 
156         when (mRecordClass.java.superclass) {
157             IntervalRecord::class.java -> {
158                 setupStartAndEndTimeFields()
159             }
160             InstantRecord::class.java -> {
161                 setupTimeField("Time", "time")
162             }
163             else -> {
164                 Toast.makeText(context, R.string.not_implemented, Toast.LENGTH_SHORT).show()
165                 mNavigationController.popBackStack()
166             }
167         }
168         setupRecordFields()
169         setupEnumFields()
170         handleSpecialCases()
171         setupListFields()
172         setupInsertDataButton(view)
173         setupUpdateDataButton(view)
174     }
175 
176     private fun setupTimeField(title: String, key: String, setPreviousDay: Boolean = false) {
177         val timeField = DateTimePicker(this.requireContext(), title, setPreviousDay)
178         mLinearLayout.addView(timeField)
179 
180         mFieldNameToFieldInput[key] = timeField
181     }
182 
183     private fun setupStartAndEndTimeFields() {
184         setupTimeField("Start Time", "startTime", true)
185         setupTimeField("End Time", "endTime")
186     }
187 
188     private fun setupRecordFields() {
189         var field: InputFieldView
190         for (mRecordsField in mRecordFields) {
191             when (mRecordsField.type) {
192                 Long::class.java -> {
193                     field =
194                         EditableTextView(this.requireContext(), mRecordsField.name, INPUT_TYPE_LONG)
195                 }
196                 ExerciseRoute::class.java, // Edge case
197                 Int::class.java, // Most of int fields are enums and are handled separately
198                 List::class
199                     .java, // Handled later so that list fields are always added towards the end
200                 -> {
201                     continue
202                 }
203                 Double::class.java,
204                 Pressure::class.java,
205                 BloodGlucose::class.java,
206                 Temperature::class.java,
207                 Volume::class.java,
208                 Percentage::class.java,
209                 Mass::class.java,
210                 Length::class.java,
211                 Energy::class.java,
212                 Power::class.java, -> {
213                     field =
214                         EditableTextView(
215                             this.requireContext(), mRecordsField.name, INPUT_TYPE_DOUBLE)
216                 }
217                 TemperatureDelta::class.java -> {
218                     field =
219                         EditableTextView(
220                             this.requireContext(), mRecordsField.name, INPUT_TYPE_DOUBLE)
221                 }
222                 CharSequence::class.java -> {
223                     field =
224                         EditableTextView(this.requireContext(), mRecordsField.name, INPUT_TYPE_TEXT)
225                 }
226                 else -> {
227                     continue
228                 }
229             }
230             mLinearLayout.addView(field)
231             mFieldNameToFieldInput[mRecordsField.name] = field
232         }
233     }
234 
235     private fun setupEnumFields() {
236         val enumFieldNameToClass: HashMap<String, KClass<*>> = HashMap()
237         var field: InputFieldView
238         when (mRecordClass) {
239             MenstruationFlowRecord::class -> {
240                 enumFieldNameToClass["mFlow"] =
241                     MenstruationFlowRecord.MenstruationFlowType::class as KClass<*>
242             }
243             OvulationTestRecord::class -> {
244                 enumFieldNameToClass["mResult"] =
245                     OvulationTestRecord.OvulationTestResult::class as KClass<*>
246             }
247             SexualActivityRecord::class -> {
248                 enumFieldNameToClass["mProtectionUsed"] =
249                     SexualActivityRecord.SexualActivityProtectionUsed::class as KClass<*>
250             }
251             CervicalMucusRecord::class -> {
252                 enumFieldNameToClass["mSensation"] =
253                     CervicalMucusRecord.CervicalMucusSensation::class as KClass<*>
254                 enumFieldNameToClass["mAppearance"] =
255                     CervicalMucusRecord.CervicalMucusAppearance::class as KClass<*>
256             }
257             Vo2MaxRecord::class -> {
258                 enumFieldNameToClass["mMeasurementMethod"] =
259                     Vo2MaxRecord.Vo2MaxMeasurementMethod::class as KClass<*>
260             }
261             BasalBodyTemperatureRecord::class -> {
262                 enumFieldNameToClass["mBodyTemperatureMeasurementLocation"] =
263                     BodyTemperatureMeasurementLocation::class as KClass<*>
264             }
265             BloodGlucoseRecord::class -> {
266                 enumFieldNameToClass["mSpecimenSource"] =
267                     BloodGlucoseRecord.SpecimenSource::class as KClass<*>
268                 enumFieldNameToClass["mRelationToMeal"] =
269                     BloodGlucoseRecord.RelationToMealType::class as KClass<*>
270                 enumFieldNameToClass["mMealType"] = MealType::class as KClass<*>
271             }
272             BloodPressureRecord::class -> {
273                 enumFieldNameToClass["mMeasurementLocation"] =
274                     BodyTemperatureMeasurementLocation::class as KClass<*>
275                 enumFieldNameToClass["mBodyPosition"] =
276                     BloodPressureRecord.BodyPosition::class as KClass<*>
277             }
278             BodyTemperatureRecord::class -> {
279                 enumFieldNameToClass["mMeasurementLocation"] =
280                     BodyTemperatureMeasurementLocation::class as KClass<*>
281             }
282             SkinTemperatureRecord::class -> {
283                 enumFieldNameToClass["mMeasurementLocation"] =
284                     SkinTemperatureRecord::class as KClass<*>
285             }
286             ExerciseSessionRecord::class -> {
287                 enumFieldNameToClass["mExerciseType"] = ExerciseSessionType::class as KClass<*>
288             }
289             PlannedExerciseSessionRecord::class -> {
290                 enumFieldNameToClass["mPlannedExerciseType"] =
291                     ExerciseSessionType::class as KClass<*>
292             }
293         }
294         if (enumFieldNameToClass.size > 0) {
295             for (entry in enumFieldNameToClass.entries) {
296                 val fieldName = entry.key
297                 val enumClass = entry.value
298                 val enumFieldsWithValues: EnumFieldsWithValues =
299                     GeneralUtils.getStaticFieldNamesAndValues(enumClass)
300                 field = EnumDropDown(this.requireContext(), fieldName, enumFieldsWithValues)
301                 mLinearLayout.addView(field)
302                 mFieldNameToFieldInput[fieldName] = field
303             }
304         }
305     }
306 
307     private fun setupListFields() {
308         var field: InputFieldView
309         for (mRecordsField in mRecordFields) {
310             when (mRecordsField.type) {
311                 List::class.java -> {
312                     field =
313                         ListInputField(
314                             this.requireContext(),
315                             mRecordsField.name,
316                             mRecordsField.genericType as ParameterizedType)
317                 }
318                 else -> {
319                     continue
320                 }
321             }
322             mLinearLayout.addView(field)
323             mFieldNameToFieldInput[mRecordsField.name] = field
324         }
325     }
326 
327     private fun handleSpecialCases() {
328         var field: InputFieldView? = null
329         var fieldName: String? = null
330 
331         when (mRecordClass) {
332             FloorsClimbedRecord::class -> {
333                 fieldName = "mFloors"
334                 field = EditableTextView(this.requireContext(), fieldName, INPUT_TYPE_INT)
335             }
336             ExerciseSessionRecord::class -> {
337                 fieldName = "mExerciseRoute"
338                 field =
339                     EnumDropDown(
340                         this.requireContext(),
341                         fieldName,
342                         EnumFieldsWithValues(routeDataMap as Map<String, Any>))
343             }
344         }
345         if (field != null && fieldName != null) {
346             mLinearLayout.addView(field)
347             mFieldNameToFieldInput[fieldName] = field
348         }
349     }
350 
351     private fun setupInsertDataButton(view: View) {
352         val buttonView = view.findViewById<Button>(R.id.insert_record)
353 
354         buttonView.setOnClickListener {
355             try {
356                 val record =
357                     createRecordObject(mRecordClass, mFieldNameToFieldInput, requireContext())
358                 mInsertOrUpdateViewModel.insertRecordsViaViewModel(
359                     listOf(record), mHealthConnectManager)
360             } catch (ex: Exception) {
361                 Log.d("InsertOrUpdateRecordsViewModel", ex.localizedMessage!!)
362                 Toast.makeText(
363                         context,
364                         "Unable to insert record: ${ex.localizedMessage}",
365                         Toast.LENGTH_SHORT)
366                     .show()
367             }
368         }
369     }
370 
371     private fun setupUpdateRecordUuidInputDialog() {
372         mUpdateRecordUuid = EditableTextView(requireContext(), null, INPUT_TYPE_TEXT)
373         val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
374         builder.setTitle("Enter UUID")
375         builder.setView(mUpdateRecordUuid)
376         builder.setPositiveButton(android.R.string.ok) { _, _ ->
377             try {
378                 if (mUpdateRecordUuid.getFieldValue().toString().isEmpty()) {
379                     throw IllegalArgumentException("Please enter UUID")
380                 }
381                 val record =
382                     createRecordObject(
383                         mRecordClass,
384                         mFieldNameToFieldInput,
385                         requireContext(),
386                         mUpdateRecordUuid.getFieldValue().toString())
387                 mInsertOrUpdateViewModel.updateRecordsViaViewModel(
388                     listOf(record), mHealthConnectManager)
389             } catch (ex: Exception) {
390                 Toast.makeText(
391                         context, "Unable to update: ${ex.localizedMessage}", Toast.LENGTH_SHORT)
392                     .show()
393             }
394         }
395         val alertDialog: AlertDialog = builder.create()
396         alertDialog.show()
397     }
398 
399     private fun setupUpdateDataButton(view: View) {
400         val buttonView = view.findViewById<Button>(R.id.update_record)
401 
402         buttonView.setOnClickListener { setupUpdateRecordUuidInputDialog() }
403     }
404 }
405