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.ExerciseSegmentType
20 import android.health.connect.datatypes.SpeedRecord
21 import android.health.connect.datatypes.SpeedRecord.SpeedRecordSample
22 import android.health.connect.datatypes.units.Velocity
23 import android.icu.text.MessageFormat
24 import android.text.format.DateUtils.formatElapsedTime
25 import androidx.annotation.StringRes
26 import com.android.healthconnect.controller.R
27 import com.android.healthconnect.controller.data.entries.FormattedEntry
28 import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedSessionDetail
29 import com.android.healthconnect.controller.dataentries.formatters.shared.EntryFormatter
30 import com.android.healthconnect.controller.dataentries.formatters.shared.RecordDetailsFormatter
31 import com.android.healthconnect.controller.dataentries.units.DistanceUnit.KILOMETERS
32 import com.android.healthconnect.controller.dataentries.units.DistanceUnit.MILES
33 import com.android.healthconnect.controller.dataentries.units.SpeedConverter.convertToDistancePerHour
34 import com.android.healthconnect.controller.dataentries.units.UnitPreferences
35 import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
36 import dagger.hilt.android.qualifiers.ApplicationContext
37 import java.util.Locale
38 import javax.inject.Inject
39 
40 /** Formatter for printing Speed series data. */
41 class SpeedFormatter @Inject constructor(@ApplicationContext private val context: Context) :
42     EntryFormatter<SpeedRecord>(context), RecordDetailsFormatter<SpeedRecord> {
43 
44     private val timeFormatter = LocalDateTimeFormatter(context)
45 
46     private val METER_TO_YARD = 1.09361
47 
formatRecordnull48     override suspend fun formatRecord(
49         record: SpeedRecord,
50         header: String,
51         headerA11y: String,
52         unitPreferences: UnitPreferences
53     ): FormattedEntry {
54         return FormattedEntry.SeriesDataEntry(
55             uuid = record.metadata.id,
56             header = header,
57             headerA11y = headerA11y,
58             title = formatValue(record, unitPreferences),
59             titleA11y = formatA11yValue(record, unitPreferences),
60             dataType = getDataType(record))
61     }
62 
formatValuenull63     override suspend fun formatValue(
64         record: SpeedRecord,
65         unitPreferences: UnitPreferences
66     ): String {
67         val res = getUnitRes(unitPreferences)
68         return formatRecord(res, record.samples, unitPreferences)
69     }
70 
formatA11yValuenull71     override suspend fun formatA11yValue(
72         record: SpeedRecord,
73         unitPreferences: UnitPreferences
74     ): String {
75         val res = getA11yUnitRes(unitPreferences)
76         return formatRecord(res, record.samples, unitPreferences)
77     }
78 
formatRecordDetailsnull79     override suspend fun formatRecordDetails(record: SpeedRecord): List<FormattedEntry> {
80         return record.samples
81             .sortedBy { it.time }
82             .map { formatSample(record.metadata.id, it, unitPreferences) }
83     }
84 
formatSamplenull85     private fun formatSample(
86         id: String,
87         sample: SpeedRecordSample,
88         unitPreferences: UnitPreferences
89     ): FormattedSessionDetail {
90         return FormattedSessionDetail(
91             uuid = id,
92             header = timeFormatter.formatTime(sample.time),
93             headerA11y = timeFormatter.formatTime(sample.time),
94             title =
95                 formatSpeedValue(
96                     getUnitRes(unitPreferences), sample.speed.inMetersPerSecond, unitPreferences),
97             titleA11y =
98                 formatSpeedValue(
99                     getA11yUnitRes(unitPreferences),
100                     sample.speed.inMetersPerSecond,
101                     unitPreferences))
102     }
103 
formatRecordnull104     private fun formatRecord(
105         @StringRes res: Int,
106         samples: List<SpeedRecordSample>,
107         unitPreferences: UnitPreferences
108     ): String {
109         if (samples.isEmpty()) {
110             return context.getString(R.string.no_data)
111         }
112         val averageSpeed = samples.sumOf { it.speed.inMetersPerSecond } / samples.size
113         return formatSpeedValue(res, averageSpeed, unitPreferences)
114     }
115 
formatSpeedValuenull116     fun formatSpeedValue(
117         @StringRes res: Int,
118         speed: Double,
119         unitPreferences: UnitPreferences
120     ): String {
121         val speedWithUnit = convertToDistancePerHour(unitPreferences.getDistanceUnit(), speed)
122         return MessageFormat.format(context.getString(res), mapOf("value" to speedWithUnit))
123     }
124 
getUnitResnull125     fun getUnitRes(unitPreferences: UnitPreferences): Int {
126         return when (unitPreferences.getDistanceUnit()) {
127             MILES -> R.string.velocity_speed_miles
128             KILOMETERS -> R.string.velocity_speed_km
129         }
130     }
131 
getA11yUnitResnull132     fun getA11yUnitRes(unitPreferences: UnitPreferences): Int {
133         return when (unitPreferences.getDistanceUnit()) {
134             MILES -> R.string.velocity_speed_miles_long
135             KILOMETERS -> R.string.velocity_speed_km_long
136         }
137     }
138 
formatSpeedValuenull139     fun formatSpeedValue(
140         speed: Velocity,
141         unitPreferences: UnitPreferences,
142         exerciseSegmentType: Int
143     ): String {
144         if (Companion.ACTIVITY_TYPES_WITH_PACE_VELOCITY.contains(exerciseSegmentType)) {
145             return formatSpeedValueToMinPerDistance(
146                 getUnitResInMinPerDistance(unitPreferences), speed, unitPreferences)
147         } else if (Companion.SWIMMING_ACTIVITY_TYPES.contains(exerciseSegmentType)) {
148             return formatSpeedValueToMinPerOneHundredDistance(
149                 getUnitResInMinPerOneHundredDistance(unitPreferences), speed, unitPreferences)
150         }
151         return formatSpeedValue(
152             getUnitRes(unitPreferences), speed.inMetersPerSecond, unitPreferences)
153     }
154 
formatA11ySpeedValuenull155     fun formatA11ySpeedValue(
156         speed: Velocity,
157         unitPreferences: UnitPreferences,
158         exerciseSegmentType: Int
159     ): String {
160         if (Companion.ACTIVITY_TYPES_WITH_PACE_VELOCITY.contains(exerciseSegmentType)) {
161             return formatSpeedValueToMinPerDistance(
162                 getA11yUnitResInMinPerDistance(unitPreferences), speed, unitPreferences)
163         }
164         if (Companion.SWIMMING_ACTIVITY_TYPES.contains(exerciseSegmentType)) {
165             return formatSpeedValueToMinPerOneHundredDistance(
166                 getA11yUnitResInMinPerOneHundredDistance(unitPreferences), speed, unitPreferences)
167         }
168         return formatSpeedValue(
169             getA11yUnitRes(unitPreferences), speed.inMetersPerSecond, unitPreferences)
170     }
171 
formatSpeedValueToMinPerDistancenull172     private fun formatSpeedValueToMinPerDistance(
173         @StringRes res: Int,
174         speed: Velocity,
175         unitPreferences: UnitPreferences
176     ): String {
177         val timePerUnitInSeconds =
178             if (speed.inMetersPerSecond != 0.0)
179                 3600 /
180                     convertToDistancePerHour(
181                         unitPreferences.getDistanceUnit(), speed.inMetersPerSecond)
182             else speed.inMetersPerSecond
183 
184         // Display "--:--" if pace value is unrealistic
185         if (timePerUnitInSeconds.toLong() > 32400) {
186             return context.getString(R.string.elapsed_time_placeholder)
187         }
188 
189         return context.getString(res, formatElapsedTime(timePerUnitInSeconds.toLong()))
190     }
191 
getUnitResInMinPerDistancenull192     private fun getUnitResInMinPerDistance(unitPreferences: UnitPreferences): Int {
193         return when (unitPreferences.getDistanceUnit()) {
194             MILES -> R.string.velocity_minute_miles
195             KILOMETERS -> R.string.velocity_minute_km
196         }
197     }
198 
getA11yUnitResInMinPerDistancenull199     private fun getA11yUnitResInMinPerDistance(unitPreferences: UnitPreferences): Int {
200         return when (unitPreferences.getDistanceUnit()) {
201             MILES -> R.string.velocity_minute_miles_long
202             KILOMETERS -> R.string.velocity_minute_km_long
203         }
204     }
205 
formatSpeedValueToMinPerOneHundredDistancenull206     private fun formatSpeedValueToMinPerOneHundredDistance(
207         @StringRes res: Int,
208         speed: Velocity,
209         unitPreferences: UnitPreferences
210     ): String {
211         val timePerUnitInSeconds =
212             if (unitPreferences.getDistanceUnit() == MILES &&
213                 Locale.getDefault().equals(Locale.US)) {
214                 val yardsPerSecond = speed.inMetersPerSecond * METER_TO_YARD
215                 if (yardsPerSecond != 0.0) 100 / yardsPerSecond else yardsPerSecond
216             } else {
217                 if (speed.inMetersPerSecond != 0.0) 100 / speed.inMetersPerSecond
218                 else speed.inMetersPerSecond
219             }
220 
221         // Display "--:--" if pace value is unrealistic
222         if (timePerUnitInSeconds.toLong() > 32400) {
223             return context.getString(R.string.elapsed_time_placeholder)
224         }
225 
226         return context.getString(res, formatElapsedTime(timePerUnitInSeconds.toLong()))
227     }
228 
getUnitResInMinPerOneHundredDistancenull229     private fun getUnitResInMinPerOneHundredDistance(unitPreferences: UnitPreferences): Int {
230         return when (unitPreferences.getDistanceUnit()) {
231             MILES ->
232                 if (Locale.getDefault().equals(Locale.US))
233                     R.string.velocity_minute_per_one_hundred_yards
234                 else R.string.velocity_minute_per_one_hundred_meters
235             KILOMETERS -> R.string.velocity_minute_per_one_hundred_meters
236         }
237     }
238 
getA11yUnitResInMinPerOneHundredDistancenull239     private fun getA11yUnitResInMinPerOneHundredDistance(unitPreferences: UnitPreferences): Int {
240         return when (unitPreferences.getDistanceUnit()) {
241             MILES ->
242                 if (Locale.getDefault().equals(Locale.US))
243                     R.string.velocity_minute_per_one_hundred_yards_long
244                 else R.string.velocity_minute_per_one_hundred_meters_long
245             KILOMETERS -> R.string.velocity_minute_per_one_hundred_meters_long
246         }
247     }
248 
249     companion object {
250         val ACTIVITY_TYPES_WITH_PACE_VELOCITY =
251             listOf(
252                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_ELLIPTICAL,
253                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_RUNNING,
254                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_RUNNING_TREADMILL,
255                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_WALKING,
256             )
257         val SWIMMING_ACTIVITY_TYPES =
258             listOf(
259                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_BACKSTROKE,
260                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_BREASTSTROKE,
261                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_BUTTERFLY,
262                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_FREESTYLE,
263                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_MIXED,
264                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_OPEN_WATER,
265                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_OTHER,
266                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_POOL)
267     }
268 }
269