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