1 /**
<lambda>null2  * 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
17 
18 import android.content.Context
19 import android.util.AttributeSet
20 import android.view.View
21 import android.view.accessibility.AccessibilityEvent
22 import android.view.accessibility.AccessibilityNodeInfo
23 import android.widget.ImageButton
24 import android.widget.TextView
25 import androidx.constraintlayout.widget.ConstraintLayout
26 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
27 import com.android.healthconnect.controller.R
28 import com.android.healthconnect.controller.utils.DatePickerFactory
29 import com.android.healthconnect.controller.utils.LocalDateTimeFormatter
30 import com.android.healthconnect.controller.utils.SystemTimeSource
31 import com.android.healthconnect.controller.utils.TimeSource
32 import com.android.healthconnect.controller.utils.getInstant
33 import com.android.healthconnect.controller.utils.logging.DataEntriesElement
34 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
35 import com.android.healthconnect.controller.utils.logging.HealthConnectLoggerEntryPoint
36 import com.android.healthconnect.controller.utils.toInstant
37 import com.android.healthconnect.controller.utils.toLocalDate
38 import dagger.hilt.android.EntryPointAccessors
39 import java.time.Duration.ofDays
40 import java.time.Instant
41 
42 /** This DateNavigationView allows the user to navigate in time to see their past data. */
43 class DateNavigationView
44 @JvmOverloads
45 constructor(
46     context: Context,
47     attrs: AttributeSet? = null,
48     defStyleAttr: Int = 0,
49     defStyleRes: Int = 0,
50     private val timeSource: TimeSource = SystemTimeSource
51 ) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
52 
53     private val logger: HealthConnectLogger
54 
55     private lateinit var previousDayButton: ImageButton
56     private lateinit var nextDayButton: ImageButton
57     private lateinit var selectedDateView: TextView
58     private val dateFormatter = LocalDateTimeFormatter(context)
59     private var selectedDate: Instant = timeSource.currentTimeMillis().toInstant()
60     private var onDateChangedListener: OnDateChangedListener? = null
61     private var maxDate: Instant? = timeSource.currentTimeMillis().toInstant()
62 
63     init {
64         val hiltEntryPoint =
65             EntryPointAccessors.fromApplication(
66                 context.applicationContext, HealthConnectLoggerEntryPoint::class.java)
67         logger = hiltEntryPoint.logger()
68 
69         val view = inflate(context, R.layout.widget_date_navigation, this)
70         bindDateTextView(view)
71         bindNextDayButton(view)
72         bindPreviousDayButton(view)
73         updateSelectedDate()
74     }
75 
76     fun setDateChangedListener(mDateChangedListener: OnDateChangedListener?) {
77         this.onDateChangedListener = mDateChangedListener
78     }
79 
80     fun setDate(date: Instant) {
81         selectedDate = date
82         updateSelectedDate()
83     }
84 
85     fun getDate(): Instant {
86         return selectedDate
87     }
88 
89     fun setMaxDate(instant: Instant?) {
90         maxDate = instant
91         updateSelectedDate()
92     }
93 
94     private fun bindNextDayButton(view: View) {
95         nextDayButton = view.findViewById(R.id.navigation_next_day) as ImageButton
96         logger.logImpression(DataEntriesElement.NEXT_DAY_BUTTON)
97         nextDayButton.setOnClickListener {
98             logger.logInteraction(DataEntriesElement.NEXT_DAY_BUTTON)
99             selectedDate = selectedDate.plus(ofDays(1))
100             updateSelectedDate()
101         }
102     }
103 
104     private fun bindPreviousDayButton(view: View) {
105         previousDayButton = view.findViewById(R.id.navigation_previous_day) as ImageButton
106         logger.logImpression(DataEntriesElement.PREVIOUS_DAY_BUTTON)
107         previousDayButton.setOnClickListener {
108             logger.logInteraction(DataEntriesElement.PREVIOUS_DAY_BUTTON)
109             selectedDate = selectedDate.minus(ofDays(1))
110             updateSelectedDate()
111         }
112     }
113 
114     private fun bindDateTextView(view: View) {
115         // TODO(b/291249677): Add log in upcoming CL.
116         selectedDateView = view.findViewById(R.id.selected_date) as TextView
117         logger.logImpression(DataEntriesElement.SELECT_DATE_BUTTON)
118         selectedDateView.setOnClickListener {
119             logger.logInteraction(DataEntriesElement.SELECT_DATE_BUTTON)
120             val datePickerDialog = DatePickerFactory.create(context, selectedDate, maxDate)
121             setMaxDate(datePickerDialog.datePicker.maxDate.toInstant())
122 
123             datePickerDialog.setOnDateSetListener { _, year, month, day ->
124                 // OnDateSetListener returns months as Int from ( 0 - 11 ), getInstant accept month
125                 // as integer from 1 - 12
126                 selectedDate = getInstant(year, month + 1, day)
127                 updateSelectedDate()
128             }
129             datePickerDialog.show()
130         }
131         selectedDateView.accessibilityDelegate =
132             object : AccessibilityDelegate() {
133                 override fun onInitializeAccessibilityNodeInfo(
134                     host: View,
135                     info: AccessibilityNodeInfo
136                 ) {
137                     super.onInitializeAccessibilityNodeInfo(host, info)
138                     info.addAction(
139                         AccessibilityNodeInfo.AccessibilityAction(
140                             AccessibilityNodeInfoCompat.ACTION_CLICK,
141                             context.getString(R.string.selected_date_view_action_description)))
142                 }
143             }
144     }
145 
146     private fun updateSelectedDate() {
147         onDateChangedListener?.onDateChanged(selectedDate)
148         selectedDateView.text = dateFormatter.formatLongDate(selectedDate)
149         selectedDateView.contentDescription = dateFormatter.formatLongDate(selectedDate)
150         selectedDateView.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
151         val curDate = selectedDate.toLocalDate().atStartOfDay()
152         nextDayButton.isEnabled =
153             if (maxDate != null) {
154                 curDate.isBefore(maxDate?.toLocalDate()?.atStartOfDay())
155             } else {
156                 true
157             }
158     }
159 
160     interface OnDateChangedListener {
161         fun onDateChanged(selectedDate: Instant)
162     }
163 }
164