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