1 /**
<lambda>null2  * 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  * ```
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.os.Bundle
19 import android.view.LayoutInflater
20 import android.view.View
21 import android.view.ViewGroup
22 import android.widget.TextView
23 import androidx.core.os.bundleOf
24 import androidx.core.view.isVisible
25 import androidx.fragment.app.Fragment
26 import androidx.fragment.app.activityViewModels
27 import androidx.fragment.app.commitNow
28 import androidx.fragment.app.viewModels
29 import androidx.navigation.fragment.findNavController
30 import androidx.recyclerview.widget.LinearLayoutManager
31 import androidx.recyclerview.widget.RecyclerView
32 import androidx.recyclerview.widget.RecyclerView.VERTICAL
33 import com.android.healthconnect.controller.R
34 import com.android.healthconnect.controller.data.entries.FormattedEntry.ExerciseSessionEntry
35 import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedAggregation
36 import com.android.healthconnect.controller.data.entries.FormattedEntry.FormattedDataEntry
37 import com.android.healthconnect.controller.data.entries.FormattedEntry.PlannedExerciseSessionEntry
38 import com.android.healthconnect.controller.data.entries.FormattedEntry.SeriesDataEntry
39 import com.android.healthconnect.controller.data.entries.FormattedEntry.SleepSessionEntry
40 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.Empty
41 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.Loading
42 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.LoadingFailed
43 import com.android.healthconnect.controller.dataentries.DataEntriesFragmentViewModel.DataEntriesFragmentState.WithData
44 import com.android.healthconnect.controller.deletion.DeletionConstants.DELETION_TYPE
45 import com.android.healthconnect.controller.deletion.DeletionConstants.END_TIME
46 import com.android.healthconnect.controller.deletion.DeletionConstants.FRAGMENT_TAG_DELETION
47 import com.android.healthconnect.controller.deletion.DeletionConstants.START_DELETION_EVENT
48 import com.android.healthconnect.controller.deletion.DeletionConstants.START_TIME
49 import com.android.healthconnect.controller.deletion.DeletionFragment
50 import com.android.healthconnect.controller.deletion.DeletionState
51 import com.android.healthconnect.controller.deletion.DeletionType
52 import com.android.healthconnect.controller.deletion.DeletionViewModel
53 import com.android.healthconnect.controller.entrydetails.DataEntryDetailsFragment
54 import com.android.healthconnect.controller.permissions.data.FitnessPermissionStrings.Companion.fromPermissionType
55 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
56 import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesFragment.Companion.PERMISSION_TYPE_KEY
57 import com.android.healthconnect.controller.shared.DataType
58 import com.android.healthconnect.controller.shared.recyclerview.RecyclerViewAdapter
59 import com.android.healthconnect.controller.utils.TimeSource
60 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
61 import com.android.healthconnect.controller.utils.logging.PageName
62 import com.android.healthconnect.controller.utils.logging.ToolbarElement
63 import com.android.healthconnect.controller.utils.setTitle
64 import com.android.healthconnect.controller.utils.setupMenu
65 import com.android.healthconnect.controller.utils.toInstant
66 import dagger.hilt.android.AndroidEntryPoint
67 import java.time.Instant
68 import javax.inject.Inject
69 
70 /** Fragment to show health data entries by date. */
71 @AndroidEntryPoint(Fragment::class)
72 class DataEntriesFragment : Hilt_DataEntriesFragment() {
73 
74     @Inject lateinit var logger: HealthConnectLogger
75     @Inject lateinit var timeSource: TimeSource
76     private val pageName = PageName.DATA_ENTRIES_PAGE
77 
78     private lateinit var permissionType: HealthPermissionType
79     private val entriesViewModel: DataEntriesFragmentViewModel by viewModels()
80     private val deletionViewModel: DeletionViewModel by activityViewModels()
81 
82     private lateinit var dateNavigationView: DateNavigationView
83     private lateinit var entriesRecyclerView: RecyclerView
84     private lateinit var noDataView: TextView
85     private lateinit var loadingView: View
86     private lateinit var errorView: View
87     private lateinit var adapter: RecyclerViewAdapter
88 
89     private val onClickEntryListener by lazy {
90         object : OnClickEntryListener {
91             override fun onItemClicked(id: String, index: Int) {
92                 findNavController()
93                     .navigate(
94                         R.id.action_dataEntriesFragment_to_dataEntryDetailsFragment,
95                         DataEntryDetailsFragment.createBundle(
96                             permissionType, id, showDataOrigin = true))
97             }
98         }
99     }
100     private val onDeleteEntryListener by lazy {
101         object : OnDeleteEntryListener {
102             override fun onDeleteEntry(
103                 id: String,
104                 dataType: DataType,
105                 index: Int,
106                 startTime: Instant?,
107                 endTime: Instant?
108             ) {
109                 deleteEntry(id, dataType, index, startTime, endTime)
110             }
111         }
112     }
113     private val aggregationViewBinder by lazy { AggregationViewBinder() }
114     private val entryViewBinder by lazy {
115         EntryItemViewBinder(onDeleteEntryListener = onDeleteEntryListener)
116     }
117     private val sleepSessionViewBinder by lazy {
118         SleepSessionItemViewBinder(
119             onDeleteEntryListenerClicked = onDeleteEntryListener,
120             onItemClickedListener = onClickEntryListener)
121     }
122     private val exerciseSessionItemViewBinder by lazy {
123         ExerciseSessionItemViewBinder(
124             onDeleteEntryClicked = onDeleteEntryListener,
125             onItemClickedListener = onClickEntryListener)
126     }
127     private val seriesDataItemViewBinder by lazy {
128         SeriesDataItemViewBinder(
129             onDeleteEntryClicked = onDeleteEntryListener,
130             onItemClickedListener = onClickEntryListener)
131     }
132     private val plannedExerciseSessionItemViewBinder by lazy {
133         PlannedExerciseSessionItemViewBinder(
134             onDeleteEntryClicked = onDeleteEntryListener,
135             onItemClickedListener = onClickEntryListener)
136     }
137 
138     override fun onCreateView(
139         inflater: LayoutInflater,
140         container: ViewGroup?,
141         savedInstanceState: Bundle?
142     ): View? {
143         logger.setPageId(pageName)
144 
145         val view = inflater.inflate(R.layout.fragment_data_entries, container, false)
146         if (requireArguments().containsKey(PERMISSION_TYPE_KEY)) {
147             permissionType =
148                 arguments?.getSerializable(PERMISSION_TYPE_KEY, HealthPermissionType::class.java)
149                     ?: throw IllegalArgumentException("PERMISSION_TYPE_KEY can't be null!")
150         }
151         setTitle(fromPermissionType(permissionType).uppercaseLabel)
152         setupMenu(R.menu.set_data_units_with_send_feedback_and_help, viewLifecycleOwner, logger) {
153             menuItem ->
154             when (menuItem.itemId) {
155                 R.id.menu_open_units -> {
156                     logger.logImpression(ToolbarElement.TOOLBAR_UNITS_BUTTON)
157                     findNavController().navigate(R.id.action_dataEntriesFragment_to_unitsFragment)
158                     true
159                 }
160                 else -> false
161             }
162         }
163         logger.logImpression(ToolbarElement.TOOLBAR_SETTINGS_BUTTON)
164 
165         dateNavigationView = view.findViewById(R.id.date_navigation_view)
166         setDateNavigationViewMaxDate()
167         noDataView = view.findViewById(R.id.no_data_view)
168         errorView = view.findViewById(R.id.error_view)
169         loadingView = view.findViewById(R.id.loading)
170         adapter =
171             RecyclerViewAdapter.Builder()
172                 .setViewBinder(FormattedDataEntry::class.java, entryViewBinder)
173                 .setViewBinder(SleepSessionEntry::class.java, sleepSessionViewBinder)
174                 .setViewBinder(ExerciseSessionEntry::class.java, exerciseSessionItemViewBinder)
175                 .setViewBinder(SeriesDataEntry::class.java, seriesDataItemViewBinder)
176                 .setViewBinder(
177                     PlannedExerciseSessionEntry::class.java, plannedExerciseSessionItemViewBinder)
178                 .setViewBinder(FormattedAggregation::class.java, aggregationViewBinder)
179                 .build()
180         entriesRecyclerView =
181             view.findViewById<RecyclerView?>(R.id.data_entries_list).also {
182                 it.adapter = adapter
183                 it.layoutManager = LinearLayoutManager(context, VERTICAL, false)
184             }
185 
186         if (childFragmentManager.findFragmentByTag(FRAGMENT_TAG_DELETION) == null) {
187             childFragmentManager.commitNow { add(DeletionFragment(), FRAGMENT_TAG_DELETION) }
188         }
189 
190         return view
191     }
192 
193     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
194         super.onViewCreated(view, savedInstanceState)
195 
196         dateNavigationView.setDateChangedListener(
197             object : DateNavigationView.OnDateChangedListener {
198                 override fun onDateChanged(selectedDate: Instant) {
199                     entriesViewModel.loadData(permissionType, selectedDate)
200                 }
201             })
202         observeDeleteState()
203         observeEntriesUpdates()
204     }
205 
206     override fun onResume() {
207         super.onResume()
208         setTitle(fromPermissionType(permissionType).uppercaseLabel)
209         if (entriesViewModel.currentSelectedDate.value != null) {
210             val date = entriesViewModel.currentSelectedDate.value!!
211             dateNavigationView.setDate(date)
212             setDateNavigationViewMaxDate()
213             entriesViewModel.loadData(permissionType, date)
214         } else {
215             entriesViewModel.loadData(permissionType, dateNavigationView.getDate())
216         }
217 
218         logger.setPageId(pageName)
219         logger.logPageImpression()
220     }
221 
222     private fun observeDeleteState() {
223         deletionViewModel.deletionParameters.observe(viewLifecycleOwner) { params ->
224             when (params.deletionState) {
225                 DeletionState.STATE_DELETION_SUCCESSFUL -> {
226                     val index = (params.deletionType as DeletionType.DeleteDataEntry).index
227                     adapter.notifyItemRemoved(index)
228                     entriesViewModel.loadData(permissionType, dateNavigationView.getDate())
229                 }
230                 else -> {
231                     // do nothing
232                 }
233             }
234         }
235     }
236 
237     private fun observeEntriesUpdates() {
238         entriesViewModel.dataEntries.observe(viewLifecycleOwner) { state ->
239             when (state) {
240                 is Loading -> {
241                     loadingView.isVisible = true
242                     noDataView.isVisible = false
243                     errorView.isVisible = false
244                     entriesRecyclerView.isVisible = false
245                 }
246                 is Empty -> {
247                     noDataView.isVisible = true
248                     loadingView.isVisible = false
249                     errorView.isVisible = false
250                     entriesRecyclerView.isVisible = false
251                 }
252                 is WithData -> {
253                     entriesRecyclerView.isVisible = true
254                     adapter.updateData(state.entries)
255                     entriesRecyclerView.scrollToPosition(0)
256                     errorView.isVisible = false
257                     noDataView.isVisible = false
258                     loadingView.isVisible = false
259                 }
260                 is LoadingFailed -> {
261                     errorView.isVisible = true
262                     loadingView.isVisible = false
263                     noDataView.isVisible = false
264                     entriesRecyclerView.isVisible = false
265                 }
266             }
267         }
268     }
269 
270     private fun setDateNavigationViewMaxDate() {
271         if (permissionType == HealthPermissionType.PLANNED_EXERCISE) {
272             // Sets the maximum date to null for the date picker to be able navigate to future dates
273             // since there can be training planned exercise session entries in future dates for
274             // planned exercise session (training plan) permission type.
275             dateNavigationView.setMaxDate(null)
276         } else {
277             // Sets the maximum date to today since there can never be data entries in future dates
278             // for the rest of permission types.
279             dateNavigationView.setMaxDate(timeSource.currentTimeMillis().toInstant())
280         }
281     }
282 
283     private fun deleteEntry(
284         uuid: String,
285         dataType: DataType,
286         index: Int,
287         startTime: Instant?,
288         endTime: Instant?
289     ) {
290         val deletionType = DeletionType.DeleteDataEntry(uuid, dataType, index)
291 
292         if (deletionType.dataType == DataType.MENSTRUATION_PERIOD) {
293             childFragmentManager.setFragmentResult(
294                 START_DELETION_EVENT,
295                 bundleOf(
296                     DELETION_TYPE to deletionType, START_TIME to startTime, END_TIME to endTime))
297         } else {
298             childFragmentManager.setFragmentResult(
299                 START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionType))
300         }
301     }
302 }
303