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