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