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