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