/** * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.android.healthconnect.controller.data.formatters import android.content.Context import android.health.connect.datatypes.SleepSessionRecord import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_AWAKE import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_AWAKE_IN_BED import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_AWAKE_OUT_OF_BED import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_DEEP import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_LIGHT import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_SLEEPING_REM import android.health.connect.datatypes.SleepSessionRecord.StageType.STAGE_TYPE_UNKNOWN import androidx.annotation.VisibleForTesting import com.android.healthconnect.controller.R import com.android.healthconnect.controller.data.entries.FormattedEntry import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedSessionDetail import com.android.healthconnect.controller.data.formatters.DurationFormatter.formatDurationLong import com.android.healthconnect.controller.data.formatters.DurationFormatter.formatDurationShort import com.android.healthconnect.controller.data.formatters.shared.BaseFormatter import com.android.healthconnect.controller.data.formatters.shared.RecordDetailsFormatter import com.android.healthconnect.controller.data.formatters.shared.UnitFormatter import com.android.healthconnect.controller.units.UnitPreferences import com.android.healthconnect.controller.utils.LocalDateTimeFormatter import dagger.hilt.android.qualifiers.ApplicationContext import java.time.Duration import javax.inject.Inject /** Formatter for printing SleepSessionRecord data. */ class SleepSessionFormatter @Inject constructor( @ApplicationContext context: Context, timeFormatter: LocalDateTimeFormatter, unitPreferences: UnitPreferences, ) : BaseFormatter(context, timeFormatter, unitPreferences), RecordDetailsFormatter, UnitFormatter { override suspend fun formatRecord( record: SleepSessionRecord, header: String, headerA11y: String, ): FormattedEntry { return FormattedEntry.SleepSessionEntry( uuid = record.metadata.id, header = header, headerA11y = headerA11y, title = formatValue(record), titleA11y = formatA11yValue(record), dataType = record::class, notes = getNotes(record), ) } @VisibleForTesting fun formatValue(record: SleepSessionRecord): String { return formatSleepSession(record) { duration -> formatDurationShort(context, duration) } } @VisibleForTesting fun formatA11yValue(record: SleepSessionRecord): String { return formatSleepSession(record) { duration -> formatDurationLong(context, duration) } } override fun formatA11yUnit(unit: Long): String { return formatDurationLong(context, Duration.ofMillis(unit)) } override fun formatUnit(unit: Long): String { return formatDurationShort(context, Duration.ofMillis(unit)) } private fun getNotes(record: SleepSessionRecord): String? { return record.notes?.toString() } private fun formatSleepSession( record: SleepSessionRecord, formatDuration: (duration: Duration) -> String, ): String { return if (!record.title.isNullOrBlank()) { context.getString(R.string.sleep_session_with_one_field, record.title) } else { val duration = Duration.between(record.startTime, record.endTime) context.getString(R.string.sleep_session_default, formatDuration(duration)) } } override suspend fun formatRecordDetails(record: SleepSessionRecord): List { val sortedStages = record.stages.sortedBy { it.startTime } return sortedStages.map { stage -> formatSleepStage(record.metadata.id, stage) } } private fun formatSleepStage(id: String, stage: SleepSessionRecord.Stage): FormattedEntry { return FormattedSessionDetail( uuid = id, header = timeFormatter.formatTimeRange(stage.startTime, stage.endTime), headerA11y = timeFormatter.formatTimeRangeA11y(stage.startTime, stage.endTime), title = formatStageType(stage) { duration -> formatDurationShort(context, duration) }, titleA11y = formatStageType(stage) { duration -> formatDurationLong(context, duration) }, ) } private fun formatStageType( stage: SleepSessionRecord.Stage, formatDuration: (duration: Duration) -> String, ): String { val stageStringRes = when (stage.type) { STAGE_TYPE_UNKNOWN -> R.string.sleep_stage_unknown STAGE_TYPE_AWAKE -> R.string.sleep_stage_awake STAGE_TYPE_AWAKE_IN_BED -> R.string.sleep_stage_awake_in_bed STAGE_TYPE_SLEEPING_DEEP -> R.string.sleep_stage_deep STAGE_TYPE_SLEEPING_LIGHT -> R.string.sleep_stage_light STAGE_TYPE_AWAKE_OUT_OF_BED -> R.string.sleep_stage_out_of_bed STAGE_TYPE_SLEEPING_REM -> R.string.sleep_stage_rem STAGE_TYPE_SLEEPING -> R.string.sleep_stage_sleeping else -> { throw IllegalArgumentException("Unrecognised sleep stage.") } } val stageString = context.getString(stageStringRes) val duration = Duration.between(stage.startTime, stage.endTime) return context.getString( R.string.sleep_stage_default, formatDuration(duration), stageString, ) } }