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