• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 = record::class,
61         )
62     }
63 
formatValuenull64     override suspend fun formatValue(
65         record: SpeedRecord,
66         unitPreferences: UnitPreferences,
67     ): String {
68         val res = getUnitRes(unitPreferences)
69         return formatRecord(res, record.samples, unitPreferences)
70     }
71 
formatA11yValuenull72     override suspend fun formatA11yValue(
73         record: SpeedRecord,
74         unitPreferences: UnitPreferences,
75     ): String {
76         val res = getA11yUnitRes(unitPreferences)
77         return formatRecord(res, record.samples, unitPreferences)
78     }
79 
formatRecordDetailsnull80     override suspend fun formatRecordDetails(record: SpeedRecord): List<FormattedEntry> {
81         return record.samples
82             .sortedBy { it.time }
83             .map { formatSample(record.metadata.id, it, unitPreferences) }
84     }
85 
formatSamplenull86     private fun formatSample(
87         id: String,
88         sample: SpeedRecordSample,
89         unitPreferences: UnitPreferences,
90     ): FormattedSessionDetail {
91         return FormattedSessionDetail(
92             uuid = id,
93             header = timeFormatter.formatTime(sample.time),
94             headerA11y = timeFormatter.formatTime(sample.time),
95             title =
96                 formatSpeedValue(
97                     getUnitRes(unitPreferences),
98                     sample.speed.inMetersPerSecond,
99                     unitPreferences,
100                 ),
101             titleA11y =
102                 formatSpeedValue(
103                     getA11yUnitRes(unitPreferences),
104                     sample.speed.inMetersPerSecond,
105                     unitPreferences,
106                 ),
107         )
108     }
109 
formatRecordnull110     private fun formatRecord(
111         @StringRes res: Int,
112         samples: List<SpeedRecordSample>,
113         unitPreferences: UnitPreferences,
114     ): String {
115         if (samples.isEmpty()) {
116             return context.getString(R.string.no_data)
117         }
118         val averageSpeed = samples.sumOf { it.speed.inMetersPerSecond } / samples.size
119         return formatSpeedValue(res, averageSpeed, unitPreferences)
120     }
121 
formatSpeedValuenull122     fun formatSpeedValue(
123         @StringRes res: Int,
124         speed: Double,
125         unitPreferences: UnitPreferences,
126     ): String {
127         val speedWithUnit = convertToDistancePerHour(unitPreferences.getDistanceUnit(), speed)
128         return MessageFormat.format(context.getString(res), mapOf("value" to speedWithUnit))
129     }
130 
getUnitResnull131     fun getUnitRes(unitPreferences: UnitPreferences): Int {
132         return when (unitPreferences.getDistanceUnit()) {
133             MILES -> R.string.velocity_speed_miles
134             KILOMETERS -> R.string.velocity_speed_km
135         }
136     }
137 
getA11yUnitResnull138     fun getA11yUnitRes(unitPreferences: UnitPreferences): Int {
139         return when (unitPreferences.getDistanceUnit()) {
140             MILES -> R.string.velocity_speed_miles_long
141             KILOMETERS -> R.string.velocity_speed_km_long
142         }
143     }
144 
formatSpeedValuenull145     fun formatSpeedValue(
146         speed: Velocity,
147         unitPreferences: UnitPreferences,
148         exerciseSegmentType: Int,
149     ): String {
150         if (Companion.ACTIVITY_TYPES_WITH_PACE_VELOCITY.contains(exerciseSegmentType)) {
151             return formatSpeedValueToMinPerDistance(
152                 getUnitResInMinPerDistance(unitPreferences),
153                 speed,
154                 unitPreferences,
155             )
156         } else if (Companion.SWIMMING_ACTIVITY_TYPES.contains(exerciseSegmentType)) {
157             return formatSpeedValueToMinPerOneHundredDistance(
158                 getUnitResInMinPerOneHundredDistance(unitPreferences),
159                 speed,
160                 unitPreferences,
161             )
162         }
163         return formatSpeedValue(
164             getUnitRes(unitPreferences),
165             speed.inMetersPerSecond,
166             unitPreferences,
167         )
168     }
169 
formatA11ySpeedValuenull170     fun formatA11ySpeedValue(
171         speed: Velocity,
172         unitPreferences: UnitPreferences,
173         exerciseSegmentType: Int,
174     ): String {
175         if (Companion.ACTIVITY_TYPES_WITH_PACE_VELOCITY.contains(exerciseSegmentType)) {
176             return formatSpeedValueToMinPerDistance(
177                 getA11yUnitResInMinPerDistance(unitPreferences),
178                 speed,
179                 unitPreferences,
180             )
181         }
182         if (Companion.SWIMMING_ACTIVITY_TYPES.contains(exerciseSegmentType)) {
183             return formatSpeedValueToMinPerOneHundredDistance(
184                 getA11yUnitResInMinPerOneHundredDistance(unitPreferences),
185                 speed,
186                 unitPreferences,
187             )
188         }
189         return formatSpeedValue(
190             getA11yUnitRes(unitPreferences),
191             speed.inMetersPerSecond,
192             unitPreferences,
193         )
194     }
195 
formatSpeedValueToMinPerDistancenull196     private fun formatSpeedValueToMinPerDistance(
197         @StringRes res: Int,
198         speed: Velocity,
199         unitPreferences: UnitPreferences,
200     ): String {
201         val timePerUnitInSeconds =
202             if (speed.inMetersPerSecond != 0.0)
203                 3600 /
204                     convertToDistancePerHour(
205                         unitPreferences.getDistanceUnit(),
206                         speed.inMetersPerSecond,
207                     )
208             else speed.inMetersPerSecond
209 
210         // Display "--:--" if pace value is unrealistic
211         if (timePerUnitInSeconds.toLong() > 32400) {
212             return context.getString(R.string.elapsed_time_placeholder)
213         }
214 
215         return context.getString(res, formatElapsedTime(timePerUnitInSeconds.toLong()))
216     }
217 
getUnitResInMinPerDistancenull218     private fun getUnitResInMinPerDistance(unitPreferences: UnitPreferences): Int {
219         return when (unitPreferences.getDistanceUnit()) {
220             MILES -> R.string.velocity_minute_miles
221             KILOMETERS -> R.string.velocity_minute_km
222         }
223     }
224 
getA11yUnitResInMinPerDistancenull225     private fun getA11yUnitResInMinPerDistance(unitPreferences: UnitPreferences): Int {
226         return when (unitPreferences.getDistanceUnit()) {
227             MILES -> R.string.velocity_minute_miles_long
228             KILOMETERS -> R.string.velocity_minute_km_long
229         }
230     }
231 
formatSpeedValueToMinPerOneHundredDistancenull232     private fun formatSpeedValueToMinPerOneHundredDistance(
233         @StringRes res: Int,
234         speed: Velocity,
235         unitPreferences: UnitPreferences,
236     ): String {
237         val timePerUnitInSeconds =
238             if (
239                 unitPreferences.getDistanceUnit() == MILES && Locale.getDefault().equals(Locale.US)
240             ) {
241                 val yardsPerSecond = speed.inMetersPerSecond * METER_TO_YARD
242                 if (yardsPerSecond != 0.0) 100 / yardsPerSecond else yardsPerSecond
243             } else {
244                 if (speed.inMetersPerSecond != 0.0) 100 / speed.inMetersPerSecond
245                 else speed.inMetersPerSecond
246             }
247 
248         // Display "--:--" if pace value is unrealistic
249         if (timePerUnitInSeconds.toLong() > 32400) {
250             return context.getString(R.string.elapsed_time_placeholder)
251         }
252 
253         return context.getString(res, formatElapsedTime(timePerUnitInSeconds.toLong()))
254     }
255 
getUnitResInMinPerOneHundredDistancenull256     private fun getUnitResInMinPerOneHundredDistance(unitPreferences: UnitPreferences): Int {
257         return when (unitPreferences.getDistanceUnit()) {
258             MILES ->
259                 if (Locale.getDefault().equals(Locale.US))
260                     R.string.velocity_minute_per_one_hundred_yards
261                 else R.string.velocity_minute_per_one_hundred_meters
262             KILOMETERS -> R.string.velocity_minute_per_one_hundred_meters
263         }
264     }
265 
getA11yUnitResInMinPerOneHundredDistancenull266     private fun getA11yUnitResInMinPerOneHundredDistance(unitPreferences: UnitPreferences): Int {
267         return when (unitPreferences.getDistanceUnit()) {
268             MILES ->
269                 if (Locale.getDefault().equals(Locale.US))
270                     R.string.velocity_minute_per_one_hundred_yards_long
271                 else R.string.velocity_minute_per_one_hundred_meters_long
272             KILOMETERS -> R.string.velocity_minute_per_one_hundred_meters_long
273         }
274     }
275 
276     companion object {
277         val ACTIVITY_TYPES_WITH_PACE_VELOCITY =
278             listOf(
279                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_ELLIPTICAL,
280                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_RUNNING,
281                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_RUNNING_TREADMILL,
282                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_WALKING,
283             )
284         val SWIMMING_ACTIVITY_TYPES =
285             listOf(
286                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_BACKSTROKE,
287                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_BREASTSTROKE,
288                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_BUTTERFLY,
289                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_FREESTYLE,
290                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_MIXED,
291                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_OPEN_WATER,
292                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_OTHER,
293                 ExerciseSegmentType.EXERCISE_SEGMENT_TYPE_SWIMMING_POOL,
294             )
295     }
296 }
297