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