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