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