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.data.entries 17 18 import android.content.Intent.EXTRA_PACKAGE_NAME 19 import android.os.Bundle 20 import android.view.LayoutInflater 21 import android.view.View 22 import android.view.ViewGroup 23 import android.widget.TextView 24 import androidx.core.view.isVisible 25 import androidx.fragment.app.Fragment 26 import androidx.fragment.app.commitNow 27 import androidx.fragment.app.viewModels 28 import androidx.navigation.fragment.findNavController 29 import androidx.recyclerview.widget.LinearLayoutManager 30 import androidx.recyclerview.widget.RecyclerView 31 import androidx.recyclerview.widget.RecyclerView.VERTICAL 32 import com.android.healthconnect.controller.R 33 import com.android.healthconnect.controller.data.appdata.AppDataFragment.Companion.PERMISSION_TYPE_KEY 34 import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.Empty 35 import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.Loading 36 import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.LoadingFailed 37 import com.android.healthconnect.controller.data.entries.EntriesViewModel.EntriesFragmentState.With 38 import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationPeriod 39 import com.android.healthconnect.controller.data.entries.datenavigation.DateNavigationView 40 import com.android.healthconnect.controller.deletion.DeletionConstants.FRAGMENT_TAG_DELETION 41 import com.android.healthconnect.controller.deletion.DeletionFragment 42 import com.android.healthconnect.controller.entrydetails.DataEntryDetailsFragment 43 import com.android.healthconnect.controller.permissions.data.FitnessPermissionStrings.Companion.fromPermissionType 44 import com.android.healthconnect.controller.permissions.data.HealthPermissionType 45 import com.android.healthconnect.controller.shared.Constants 46 import com.android.healthconnect.controller.shared.recyclerview.RecyclerViewAdapter 47 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger 48 import com.android.healthconnect.controller.utils.logging.ToolbarElement 49 import com.android.healthconnect.controller.utils.setTitle 50 import com.android.healthconnect.controller.utils.setupMenu 51 import com.android.settingslib.widget.AppHeaderPreference 52 import dagger.hilt.android.AndroidEntryPoint 53 import java.time.Instant 54 import javax.inject.Inject 55 56 /** Fragment to show health data entries by date. */ 57 @AndroidEntryPoint(Fragment::class) 58 class AppEntriesFragment : Hilt_AppEntriesFragment() { 59 60 @Inject lateinit var logger: HealthConnectLogger 61 62 private var packageName: String = "" 63 private var appName: String = "" 64 65 private lateinit var permissionType: HealthPermissionType 66 private val entriesViewModel: EntriesViewModel by viewModels() 67 68 private lateinit var header: AppHeaderPreference 69 private lateinit var dateNavigationView: DateNavigationView 70 private lateinit var entriesRecyclerView: RecyclerView 71 private lateinit var noDataView: TextView 72 private lateinit var loadingView: View 73 private lateinit var errorView: View 74 private lateinit var adapter: RecyclerViewAdapter 75 76 private val onClickEntryListener by lazy { 77 object : OnClickEntryListener { 78 override fun onItemClicked(id: String, index: Int) { 79 findNavController() 80 .navigate( 81 R.id.action_appEntriesFragment_to_dataEntryDetailsFragment, 82 DataEntryDetailsFragment.createBundle( 83 permissionType, id, showDataOrigin = false)) 84 } 85 } 86 } 87 private val aggregationViewBinder by lazy { AggregationViewBinder() } 88 private val entryViewBinder by lazy { EntryItemViewBinder() } 89 private val sectionTitleViewBinder by lazy { SectionTitleViewBinder() } 90 private val sleepSessionViewBinder by lazy { 91 SleepSessionItemViewBinder(onItemClickedListener = onClickEntryListener) 92 } 93 private val exerciseSessionItemViewBinder by lazy { 94 ExerciseSessionItemViewBinder(onItemClickedListener = onClickEntryListener) 95 } 96 private val seriesDataItemViewBinder by lazy { 97 SeriesDataItemViewBinder(onItemClickedListener = onClickEntryListener) 98 } 99 100 override fun onCreateView( 101 inflater: LayoutInflater, 102 container: ViewGroup?, 103 savedInstanceState: Bundle? 104 ): View? { 105 // TODO(b/291249677): Log pagename. 106 107 if (requireArguments().containsKey(EXTRA_PACKAGE_NAME) && 108 requireArguments().getString(EXTRA_PACKAGE_NAME) != null) { 109 packageName = requireArguments().getString(EXTRA_PACKAGE_NAME)!! 110 } 111 if (requireArguments().containsKey(Constants.EXTRA_APP_NAME) && 112 requireArguments().getString(Constants.EXTRA_APP_NAME) != null) { 113 appName = requireArguments().getString(Constants.EXTRA_APP_NAME)!! 114 } 115 116 val view = inflater.inflate(R.layout.fragment_entries, container, false) 117 if (requireArguments().containsKey(PERMISSION_TYPE_KEY)) { 118 permissionType = 119 arguments?.getSerializable(PERMISSION_TYPE_KEY, HealthPermissionType::class.java) 120 ?: throw IllegalArgumentException("PERMISSION_TYPE_KEY can't be null!") 121 } 122 setTitle(fromPermissionType(permissionType).uppercaseLabel) 123 setupMenu(R.menu.set_data_units_with_send_feedback_and_help, viewLifecycleOwner, logger) { 124 menuItem -> 125 when (menuItem.itemId) { 126 R.id.menu_open_units -> { 127 logger.logImpression(ToolbarElement.TOOLBAR_UNITS_BUTTON) 128 findNavController().navigate(R.id.action_dataEntriesFragment_to_unitsFragment) 129 true 130 } 131 else -> false 132 } 133 } 134 135 dateNavigationView = view.findViewById(R.id.date_navigation_view) 136 noDataView = view.findViewById(R.id.no_data_view) 137 errorView = view.findViewById(R.id.error_view) 138 loadingView = view.findViewById(R.id.loading) 139 adapter = 140 RecyclerViewAdapter.Builder() 141 .setViewBinder(FormattedEntry.FormattedDataEntry::class.java, entryViewBinder) 142 .setViewBinder(FormattedEntry.SleepSessionEntry::class.java, sleepSessionViewBinder) 143 .setViewBinder( 144 FormattedEntry.ExerciseSessionEntry::class.java, exerciseSessionItemViewBinder) 145 .setViewBinder(FormattedEntry.SeriesDataEntry::class.java, seriesDataItemViewBinder) 146 .setViewBinder( 147 FormattedEntry.FormattedAggregation::class.java, aggregationViewBinder) 148 .setViewBinder( 149 FormattedEntry.EntryDateSectionHeader::class.java, sectionTitleViewBinder) 150 .build() 151 entriesRecyclerView = 152 view.findViewById<RecyclerView?>(R.id.data_entries_list).also { 153 it.adapter = adapter 154 it.layoutManager = LinearLayoutManager(context, VERTICAL, false) 155 } 156 157 if (childFragmentManager.findFragmentByTag(FRAGMENT_TAG_DELETION) == null) { 158 childFragmentManager.commitNow { add(DeletionFragment(), FRAGMENT_TAG_DELETION) } 159 } 160 161 return view 162 } 163 164 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 165 super.onViewCreated(view, savedInstanceState) 166 167 dateNavigationView.setDateChangedListener( 168 object : DateNavigationView.OnDateChangedListener { 169 override fun onDateChanged( 170 displayedStartDate: Instant, 171 period: DateNavigationPeriod 172 ) { 173 entriesViewModel.loadEntries( 174 permissionType, packageName, displayedStartDate, period) 175 } 176 }) 177 178 header = AppHeaderPreference(requireContext()) 179 entriesViewModel.loadAppInfo(packageName) 180 181 entriesViewModel.appInfo.observe(viewLifecycleOwner) { appMetadata -> 182 header.apply { 183 icon = appMetadata.icon 184 title = appMetadata.appName 185 } 186 } 187 188 observeEntriesUpdates() 189 } 190 191 override fun onResume() { 192 super.onResume() 193 setTitle(fromPermissionType(permissionType).uppercaseLabel) 194 if (entriesViewModel.currentSelectedDate.value != null && 195 entriesViewModel.period.value != null) { 196 val date = entriesViewModel.currentSelectedDate.value!! 197 val selectedPeriod = entriesViewModel.period.value!! 198 dateNavigationView.setDate(date) 199 dateNavigationView.setPeriod(selectedPeriod) 200 entriesViewModel.loadEntries(permissionType, packageName, date, selectedPeriod) 201 } else { 202 entriesViewModel.loadEntries( 203 permissionType, 204 packageName, 205 dateNavigationView.getDate(), 206 dateNavigationView.getPeriod()) 207 } 208 209 // TODO(b/291249677): Log pagename. 210 } 211 212 private fun observeEntriesUpdates() { 213 entriesViewModel.entries.observe(viewLifecycleOwner) { state -> 214 when (state) { 215 is Loading -> { 216 loadingView.isVisible = true 217 noDataView.isVisible = false 218 errorView.isVisible = false 219 entriesRecyclerView.isVisible = false 220 } 221 is Empty -> { 222 noDataView.isVisible = true 223 loadingView.isVisible = false 224 errorView.isVisible = false 225 entriesRecyclerView.isVisible = false 226 } 227 is With -> { 228 entriesRecyclerView.isVisible = true 229 adapter.updateData(state.entries) 230 errorView.isVisible = false 231 noDataView.isVisible = false 232 loadingView.isVisible = false 233 } 234 is LoadingFailed -> { 235 errorView.isVisible = true 236 loadingView.isVisible = false 237 noDataView.isVisible = false 238 entriesRecyclerView.isVisible = false 239 } 240 } 241 } 242 } 243 } 244