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