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 = record::class, 59 ) 60 } 61 formatRecordDetailsnull62 override suspend fun formatRecordDetails(record: SkinTemperatureRecord): List<FormattedEntry> { 63 64 val measurementLocationEntry: List<FormattedEntry> = 65 if (record.measurementLocation == MEASUREMENT_LOCATION_UNKNOWN) { 66 listOf() 67 } else { 68 listOf( 69 FormattedEntry.ReverseSessionDetail( 70 uuid = record.metadata.id, 71 title = 72 context.getString(R.string.skin_temperature_measurement_location_title), 73 titleA11y = 74 context.getString(R.string.skin_temperature_measurement_location_title), 75 header = formatLocation(context, record.measurementLocation), 76 headerA11y = formatLocation(context, record.measurementLocation), 77 ) 78 ) 79 } 80 81 val baselineEntry: List<FormattedEntry> = 82 if (record.baseline == null) { 83 listOf() 84 } else { 85 listOf( 86 FormattedEntry.ReverseSessionDetail( 87 uuid = record.metadata.id, 88 title = context.getString(R.string.skin_temperature_baseline_title), 89 titleA11y = context.getString(R.string.skin_temperature_baseline_title), 90 header = formatNullableTemperature(record.baseline, false), 91 headerA11y = formatNullableTemperature(record.baseline, true), 92 ) 93 ) 94 } 95 96 val deltasTitle: List<FormattedEntry> = 97 listOf( 98 FormattedEntry.FormattedSectionTitle( 99 context.getString(R.string.skin_temperature_delta_details_heading) 100 ) 101 ) 102 103 val deltas = 104 record.deltas 105 .sortedBy { it.time } 106 .map { formatDelta(record.metadata.id, it, unitPreferences) } 107 val formattedSectionDetails: List<FormattedEntry.FormattedSessionDetail> = buildList { 108 if (deltas.isNotEmpty()) { 109 addAll(deltas) 110 } 111 } 112 113 return measurementLocationEntry 114 .plus(baselineEntry) 115 .plus(deltasTitle) 116 .plus(formattedSectionDetails) 117 } 118 formatNullableTemperaturenull119 private fun formatNullableTemperature(temperature: Temperature?, isA11y: Boolean): String { 120 if (temperature == null) return "" 121 return if (isA11y) { 122 TemperatureFormatter.formatA11tValue( 123 context, 124 temperature, 125 MEASUREMENT_LOCATION_UNKNOWN, 126 unitPreferences, 127 ) 128 } else { 129 TemperatureFormatter.formatValue( 130 context, 131 temperature, 132 MEASUREMENT_LOCATION_UNKNOWN, 133 unitPreferences, 134 ) 135 } 136 } 137 formatDeltanull138 private fun formatDelta( 139 id: String, 140 delta: Delta, 141 unitPreferences: UnitPreferences, 142 ): FormattedEntry.FormattedSessionDetail { 143 return FormattedEntry.FormattedSessionDetail( 144 uuid = id, 145 header = timeFormatter.formatTime(delta.time), 146 headerA11y = timeFormatter.formatTime(delta.time), 147 title = 148 TemperatureDeltaFormatter.formatSingleDeltaValue( 149 context, 150 delta.delta, 151 unitPreferences, 152 ), 153 titleA11y = 154 TemperatureDeltaFormatter.formatSingleDeltaA11yValue( 155 context, 156 delta.delta, 157 unitPreferences, 158 ), 159 ) 160 } 161 formatValuenull162 override suspend fun formatValue( 163 record: SkinTemperatureRecord, 164 unitPreferences: UnitPreferences, 165 ): String { 166 return if (record.deltas.size == 1) { 167 formatUnit(record.deltas.first().delta) 168 } else { 169 format(record, unitPreferences, false) 170 } 171 } 172 formatA11yValuenull173 override suspend fun formatA11yValue( 174 record: SkinTemperatureRecord, 175 unitPreferences: UnitPreferences, 176 ): String { 177 return if (record.deltas.size == 1) { 178 formatA11yUnit(record.deltas.first().delta) 179 } else { 180 format(record, unitPreferences, true) 181 } 182 } 183 formatUnitnull184 override fun formatUnit(unit: TemperatureDelta): String { 185 return TemperatureDeltaFormatter.formatAverageDeltaValue(context, unit, unitPreferences) 186 } 187 formatA11yUnitnull188 override fun formatA11yUnit(unit: TemperatureDelta): String { 189 return TemperatureDeltaFormatter.formatAverageDeltaA11yValue(context, unit, unitPreferences) 190 } 191 formatnull192 private fun format( 193 record: SkinTemperatureRecord, 194 unitPreferences: UnitPreferences, 195 isA11y: Boolean, 196 ): String { 197 if (record.deltas.isEmpty()) { 198 return context.getString(R.string.no_data) 199 } 200 val averageDelta = 201 TemperatureDelta.fromCelsius( 202 record.deltas.sumOf { it.delta.inCelsius } / record.deltas.size 203 ) 204 return if (isA11y) { 205 TemperatureDeltaFormatter.formatAverageDeltaA11yValue( 206 context, 207 averageDelta, 208 unitPreferences, 209 ) 210 } else { 211 TemperatureDeltaFormatter.formatAverageDeltaValue( 212 context, 213 averageDelta, 214 unitPreferences, 215 ) 216 } 217 } 218 formatLocationnull219 private fun formatLocation(context: Context, location: Int): String { 220 return when (location) { 221 SkinTemperatureRecord.MEASUREMENT_LOCATION_FINGER -> 222 context.getString(R.string.temperature_location_finger) 223 SkinTemperatureRecord.MEASUREMENT_LOCATION_TOE -> 224 context.getString(R.string.temperature_location_toe) 225 SkinTemperatureRecord.MEASUREMENT_LOCATION_WRIST -> 226 context.getString(R.string.temperature_location_wrist) 227 else -> { 228 throw IllegalArgumentException( 229 "Unrecognised skin temperature measurement location: $location" 230 ) 231 } 232 } 233 } 234 } 235