1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 * 17 */ 18 19 package com.android.healthconnect.controller.dataentries.formatters 20 21 import android.content.Context 22 import android.health.connect.datatypes.MenstruationPeriodRecord 23 import android.health.connect.datatypes.Record 24 import android.icu.text.MessageFormat.format 25 import com.android.healthconnect.controller.R 26 import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedDataEntry 27 import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod 28 import com.android.healthconnect.controller.shared.app.AppInfoReader 29 import com.android.healthconnect.controller.utils.LocalDateTimeFormatter 30 import com.android.healthconnect.controller.utils.TimeSource 31 import com.android.healthconnect.controller.utils.isLessThanOneYearAgo 32 import com.android.healthconnect.controller.utils.toLocalDate 33 import com.android.healthconnect.controller.utils.toLocalTime 34 import dagger.hilt.android.qualifiers.ApplicationContext 35 import java.time.Instant 36 import java.time.LocalTime 37 import java.time.Period 38 import java.time.temporal.ChronoUnit.DAYS 39 import javax.inject.Inject 40 41 /** Formatter for printing MenstruationPeriodRecord data. */ 42 class MenstruationPeriodFormatter 43 @Inject 44 constructor( 45 private val appInfoReader: AppInfoReader, 46 @ApplicationContext private val context: Context, 47 private val timeSource: TimeSource, 48 ) { 49 50 val dateFormatter = LocalDateTimeFormatter(context) 51 formatnull52 suspend fun format( 53 day: Instant, 54 record: MenstruationPeriodRecord, 55 period: DateNavigationPeriod, 56 showDataOrigin: Boolean = true, 57 ): FormattedDataEntry { 58 val totalDays = totalDaysOfPeriod(record) 59 val appName = if (showDataOrigin) getAppName(record) else "" 60 val header = getHeader(record.startTime, record.endTime, appName) 61 return when (period) { 62 DateNavigationPeriod.PERIOD_DAY -> { 63 val dayOfPeriod = dayOfPeriod(record, day) 64 val title = context.getString(R.string.period_day, dayOfPeriod, totalDays) 65 FormattedDataEntry( 66 uuid = record.metadata.id, 67 title = title, 68 titleA11y = title, 69 header = header, 70 headerA11y = header, 71 dataType = MenstruationPeriodRecord::class, 72 startTime = record.startTime, 73 endTime = record.endTime, 74 ) 75 } 76 77 else -> { 78 val title = 79 format(context.getString(R.string.period_length), mapOf("count" to totalDays)) 80 FormattedDataEntry( 81 uuid = record.metadata.id, 82 title = title, 83 titleA11y = title, 84 header = header, 85 headerA11y = header, 86 dataType = MenstruationPeriodRecord::class, 87 startTime = record.startTime, 88 endTime = record.endTime, 89 ) 90 } 91 } 92 } 93 dayOfPeriodnull94 private fun dayOfPeriod(record: MenstruationPeriodRecord, day: Instant): Int { 95 return (Period.between(record.startTime.toLocalDate(), day.toLocalDate()).days + 96 1) // + 1 to return a 1-indexed counter (i.e. "Period day 1", not "day 0") 97 } 98 totalDaysOfPeriodnull99 private fun totalDaysOfPeriod(record: MenstruationPeriodRecord): Int { 100 return (DAYS.between(record.startTime.toLocalDate(), record.endTime.toLocalDate()).toInt() + 101 1) 102 } 103 getAppNamenull104 private suspend fun getAppName(record: Record): String { 105 return appInfoReader.getAppMetadata(record.metadata.dataOrigin.packageName).appName 106 } 107 getHeadernull108 private fun getHeader(startDate: Instant, endDate: Instant, appName: String): String { 109 if (appName == "") 110 return context.getString( 111 R.string.data_entry_header_date_range_without_source_app, 112 getDateRange(startDate, endDate), 113 ) 114 return context.getString( 115 R.string.data_entry_header_date_range_with_source_app, 116 getDateRange(startDate, endDate), 117 appName, 118 ) 119 } 120 getDateRangenull121 private fun getDateRange(startDate: Instant, endDate: Instant): String { 122 return if (endDate != startDate) { 123 var localEndDate: Instant = endDate 124 125 // If endDate is midnight, add one millisecond so that DateUtils 126 // correctly formats it as a separate date. 127 if (endDate.toLocalTime() == LocalTime.MIDNIGHT) { 128 localEndDate = endDate.plusMillis(1) 129 } 130 // display date range 131 if ( 132 startDate.isLessThanOneYearAgo(timeSource) && 133 localEndDate.isLessThanOneYearAgo(timeSource) 134 ) { 135 dateFormatter.formatDateRangeWithoutYear(startDate, localEndDate) 136 } else { 137 dateFormatter.formatDateRangeWithYear(startDate, localEndDate) 138 } 139 } else { 140 // display only one date 141 if (startDate.isLessThanOneYearAgo(timeSource)) { 142 dateFormatter.formatShortDate(startDate) 143 } else { 144 dateFormatter.formatLongDate(startDate) 145 } 146 } 147 } 148 } 149