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