1 /** 2 * 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.controller.dataentries.formatters 17 18 import android.content.Context 19 import android.health.connect.datatypes.SkinTemperatureRecord 20 import android.health.connect.datatypes.SkinTemperatureRecord.Delta 21 import android.health.connect.datatypes.SkinTemperatureRecord.MEASUREMENT_LOCATION_UNKNOWN 22 import android.health.connect.datatypes.units.Temperature 23 import android.health.connect.datatypes.units.TemperatureDelta 24 import com.android.healthconnect.controller.R 25 import com.android.healthconnect.controller.data.entries.FormattedEntry 26 import com.android.healthconnect.controller.dataentries.formatters.shared.EntryFormatter 27 import com.android.healthconnect.controller.dataentries.formatters.shared.RecordDetailsFormatter 28 import com.android.healthconnect.controller.dataentries.formatters.shared.UnitFormatter 29 import com.android.healthconnect.controller.dataentries.units.UnitPreferences 30 import com.android.healthconnect.controller.utils.LocalDateTimeFormatter 31 import dagger.hilt.android.qualifiers.ApplicationContext 32 import javax.inject.Inject 33 import javax.inject.Singleton 34 35 /** Formatter for printing SkinTemperatureRecord data. */ 36 @Singleton 37 class SkinTemperatureFormatter 38 @Inject 39 constructor(@ApplicationContext private val context: Context) : 40 EntryFormatter<SkinTemperatureRecord>(context), 41 RecordDetailsFormatter<SkinTemperatureRecord>, 42 UnitFormatter<TemperatureDelta> { 43 44 private val timeFormatter = LocalDateTimeFormatter(context) 45 formatRecordnull46 override suspend fun formatRecord( 47 record: SkinTemperatureRecord, 48 header: String, 49 headerA11y: String, 50 unitPreferences: UnitPreferences 51 ): FormattedEntry { 52 return FormattedEntry.SeriesDataEntry( 53 uuid = record.metadata.id, 54 header = header, 55 headerA11y = headerA11y, 56 title = formatValue(record, unitPreferences), 57 titleA11y = formatA11yValue(record, unitPreferences), 58 dataType = getDataType(record)) 59 } 60 formatRecordDetailsnull61 override suspend fun formatRecordDetails(record: SkinTemperatureRecord): List<FormattedEntry> { 62 63 val measurementLocationEntry: List<FormattedEntry> = 64 if (record.measurementLocation == MEASUREMENT_LOCATION_UNKNOWN) { 65 listOf() 66 } else { 67 listOf( 68 FormattedEntry.ReverseSessionDetail( 69 uuid = record.metadata.id, 70 title = 71 context.getString(R.string.skin_temperature_measurement_location_title), 72 titleA11y = 73 context.getString(R.string.skin_temperature_measurement_location_title), 74 header = formatLocation(context, record.measurementLocation), 75 headerA11y = formatLocation(context, record.measurementLocation))) 76 } 77 78 val baselineEntry: List<FormattedEntry> = 79 if (record.baseline == null) { 80 listOf() 81 } else { 82 listOf( 83 FormattedEntry.ReverseSessionDetail( 84 uuid = record.metadata.id, 85 title = context.getString(R.string.skin_temperature_baseline_title), 86 titleA11y = context.getString(R.string.skin_temperature_baseline_title), 87 header = formatNullableTemperature(record.baseline, false), 88 headerA11y = formatNullableTemperature(record.baseline, true))) 89 } 90 91 val deltasTitle: List<FormattedEntry> = 92 listOf( 93 FormattedEntry.FormattedSectionTitle( 94 context.getString(R.string.skin_temperature_delta_details_heading))) 95 96 val deltas = 97 record.deltas 98 .sortedBy { it.time } 99 .map { formatDelta(record.metadata.id, it, unitPreferences) } 100 val formattedSectionDetails: List<FormattedEntry.FormattedSessionDetail> = buildList { 101 if (deltas.isNotEmpty()) { 102 addAll(deltas) 103 } 104 } 105 106 return measurementLocationEntry 107 .plus(baselineEntry) 108 .plus(deltasTitle) 109 .plus(formattedSectionDetails) 110 } 111 formatNullableTemperaturenull112 private fun formatNullableTemperature(temperature: Temperature?, isA11y: Boolean): String { 113 if (temperature == null) return "" 114 return if (isA11y) { 115 TemperatureFormatter.formatA11tValue( 116 context, temperature, MEASUREMENT_LOCATION_UNKNOWN, unitPreferences) 117 } else { 118 TemperatureFormatter.formatValue( 119 context, temperature, MEASUREMENT_LOCATION_UNKNOWN, unitPreferences) 120 } 121 } 122 formatDeltanull123 private fun formatDelta( 124 id: String, 125 delta: Delta, 126 unitPreferences: UnitPreferences 127 ): FormattedEntry.FormattedSessionDetail { 128 return FormattedEntry.FormattedSessionDetail( 129 uuid = id, 130 header = timeFormatter.formatTime(delta.time), 131 headerA11y = timeFormatter.formatTime(delta.time), 132 title = 133 TemperatureDeltaFormatter.formatSingleDeltaValue( 134 context, delta.delta, unitPreferences), 135 titleA11y = 136 TemperatureDeltaFormatter.formatSingleDeltaA11yValue( 137 context, delta.delta, unitPreferences)) 138 } 139 formatValuenull140 override suspend fun formatValue( 141 record: SkinTemperatureRecord, 142 unitPreferences: UnitPreferences, 143 ): String { 144 return if (record.deltas.size == 1) { 145 formatUnit(record.deltas.first().delta) 146 } else { 147 format(record, unitPreferences, false) 148 } 149 } 150 formatA11yValuenull151 override suspend fun formatA11yValue( 152 record: SkinTemperatureRecord, 153 unitPreferences: UnitPreferences 154 ): String { 155 return if (record.deltas.size == 1) { 156 formatA11yUnit(record.deltas.first().delta) 157 } else { 158 format(record, unitPreferences, true) 159 } 160 } 161 formatUnitnull162 override fun formatUnit(unit: TemperatureDelta): String { 163 return TemperatureDeltaFormatter.formatAverageDeltaValue(context, unit, unitPreferences) 164 } 165 formatA11yUnitnull166 override fun formatA11yUnit(unit: TemperatureDelta): String { 167 return TemperatureDeltaFormatter.formatAverageDeltaA11yValue(context, unit, unitPreferences) 168 } 169 formatnull170 private fun format( 171 record: SkinTemperatureRecord, 172 unitPreferences: UnitPreferences, 173 isA11y: Boolean 174 ): String { 175 if (record.deltas.isEmpty()) { 176 return context.getString(R.string.no_data) 177 } 178 val averageDelta = 179 TemperatureDelta.fromCelsius( 180 record.deltas.sumOf { it.delta.inCelsius } / record.deltas.size) 181 return if (isA11y) { 182 TemperatureDeltaFormatter.formatAverageDeltaA11yValue( 183 context, averageDelta, unitPreferences) 184 } else { 185 TemperatureDeltaFormatter.formatAverageDeltaValue( 186 context, averageDelta, unitPreferences) 187 } 188 } 189 formatLocationnull190 private fun formatLocation(context: Context, location: Int): String { 191 return when (location) { 192 SkinTemperatureRecord.MEASUREMENT_LOCATION_FINGER -> 193 context.getString(R.string.temperature_location_finger) 194 SkinTemperatureRecord.MEASUREMENT_LOCATION_TOE -> 195 context.getString(R.string.temperature_location_toe) 196 SkinTemperatureRecord.MEASUREMENT_LOCATION_WRIST -> 197 context.getString(R.string.temperature_location_wrist) 198 else -> { 199 throw IllegalArgumentException( 200 "Unrecognised skin temperature measurement location: $location") 201 } 202 } 203 } 204 } 205