1 /** 2 * Copyright (C) 2023 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 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package com.android.healthconnect.controller.shared.preference 15 16 import android.content.Context 17 import android.util.AttributeSet 18 import android.view.LayoutInflater 19 import android.widget.ImageView 20 import android.widget.LinearLayout 21 import android.widget.TextView 22 import androidx.constraintlayout.widget.ConstraintLayout 23 import androidx.constraintlayout.widget.ConstraintSet 24 import com.android.healthconnect.controller.R 25 import com.android.healthconnect.controller.datasources.AggregationCardInfo 26 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.fromHealthPermissionType 27 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.icon 28 import com.android.healthconnect.controller.utils.LocalDateTimeFormatter 29 import com.android.healthconnect.controller.utils.SystemTimeSource 30 import com.android.healthconnect.controller.utils.TimeSource 31 import com.android.healthconnect.controller.utils.toLocalTime 32 import java.time.Instant 33 import java.time.LocalTime 34 35 /** A custom card to display the latest available data aggregations. */ 36 class AggregationDataCard 37 @JvmOverloads 38 constructor( 39 context: Context, 40 attrs: AttributeSet? = null, 41 cardType: CardTypeEnum, 42 cardInfo: AggregationCardInfo, 43 private val timeSource: TimeSource = SystemTimeSource 44 ) : LinearLayout(context, attrs) { 45 private val dateFormatter = LocalDateTimeFormatter(context) 46 47 init { 48 LayoutInflater.from(context).inflate(R.layout.widget_aggregation_data_card, this, true) 49 50 val cardIcon = findViewById<ImageView>(R.id.card_icon) 51 val cardTitle = findViewById<TextView>(R.id.card_title_number) 52 val cardDate = findViewById<TextView>(R.id.card_date) 53 val titleAndDateContainer = findViewById<ConstraintLayout>(R.id.title_date_container) 54 55 cardIcon.background = fromHealthPermissionType(cardInfo.healthPermissionType).icon(context) 56 cardTitle.text = cardInfo.aggregation.aggregation 57 cardTitle.contentDescription = cardInfo.aggregation.aggregationA11y 58 59 val totalStartDate = cardInfo.startDate 60 val totalEndDate = cardInfo.endDate 61 cardDate.text = formatDateText(totalStartDate, totalEndDate) 62 63 val constraintSet = ConstraintSet() 64 constraintSet.clone(titleAndDateContainer) 65 66 // Rearrange the textViews based on the type of card 67 if (cardType == CardTypeEnum.SMALL_CARD) { 68 constraintSet.connect( 69 R.id.card_title_number, 70 ConstraintSet.START, 71 ConstraintSet.PARENT_ID, 72 ConstraintSet.START) 73 constraintSet.connect( 74 R.id.card_title_number, 75 ConstraintSet.TOP, 76 ConstraintSet.PARENT_ID, 77 ConstraintSet.TOP) 78 constraintSet.connect( 79 R.id.card_title_number, ConstraintSet.BOTTOM, R.id.card_date, ConstraintSet.TOP) 80 81 constraintSet.connect( 82 R.id.card_date, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) 83 constraintSet.connect( 84 R.id.card_date, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM) 85 constraintSet.connect( 86 R.id.card_date, ConstraintSet.TOP, R.id.card_title_number, ConstraintSet.BOTTOM) 87 } else { 88 constraintSet.connect( 89 R.id.card_title_number, 90 ConstraintSet.START, 91 ConstraintSet.PARENT_ID, 92 ConstraintSet.START) 93 constraintSet.connect( 94 R.id.card_title_number, 95 ConstraintSet.TOP, 96 ConstraintSet.PARENT_ID, 97 ConstraintSet.TOP) 98 constraintSet.connect( 99 R.id.card_title_number, 100 ConstraintSet.BOTTOM, 101 ConstraintSet.PARENT_ID, 102 ConstraintSet.BOTTOM) 103 constraintSet.connect( 104 R.id.card_title_number, ConstraintSet.END, R.id.card_date, ConstraintSet.START) 105 106 constraintSet.connect( 107 R.id.card_date, ConstraintSet.START, R.id.card_title_number, ConstraintSet.END) 108 constraintSet.connect( 109 R.id.card_date, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) 110 constraintSet.connect( 111 R.id.card_date, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP) 112 constraintSet.connect( 113 R.id.card_date, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM) 114 115 constraintSet.createHorizontalChain( 116 ConstraintSet.PARENT_ID, 117 ConstraintSet.LEFT, 118 ConstraintSet.PARENT_ID, 119 ConstraintSet.RIGHT, 120 intArrayOf(R.id.card_title_number, R.id.card_date), 121 null, 122 ConstraintSet.CHAIN_SPREAD_INSIDE) 123 124 constraintSet.constrainedWidth(R.id.card_title_number, true) 125 constraintSet.constrainedWidth(R.id.card_date, true) 126 } 127 128 constraintSet.applyTo(titleAndDateContainer) 129 } 130 isLessThanOneYearAgonull131 private fun isLessThanOneYearAgo(instant: Instant): Boolean { 132 val oneYearAgo = 133 timeSource 134 .currentLocalDateTime() 135 .minusYears(1) 136 .toLocalDate() 137 .atStartOfDay(timeSource.deviceZoneOffset()) 138 .toInstant() 139 return instant.isAfter(oneYearAgo) 140 } 141 formatDateTextnull142 private fun formatDateText(startDate: Instant, endDate: Instant?): String { 143 return if (endDate != null) { 144 var localEndDate: Instant = endDate 145 146 // If endDate is midnight, add one millisecond so that DateUtils 147 // correctly formats it as a separate date. 148 if (endDate.toLocalTime() == LocalTime.MIDNIGHT) { 149 localEndDate = endDate.plusMillis(1) 150 } 151 // display date range 152 if (isLessThanOneYearAgo(startDate) && isLessThanOneYearAgo(localEndDate)) { 153 dateFormatter.formatDateRangeWithoutYear(startDate, localEndDate) 154 } else { 155 dateFormatter.formatDateRangeWithYear(startDate, localEndDate) 156 } 157 } else { 158 // display only one date 159 if (isLessThanOneYearAgo(startDate)) { 160 dateFormatter.formatShortDate(startDate) 161 } else { 162 dateFormatter.formatLongDate(startDate) 163 } 164 } 165 } 166 167 enum class CardTypeEnum { 168 SMALL_CARD, 169 LARGE_CARD 170 } 171 } 172