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.alldata
17 
18 import android.graphics.drawable.Drawable
19 import android.os.Bundle
20 import android.view.MenuItem
21 import android.view.View
22 import androidx.annotation.VisibleForTesting
23 import androidx.core.os.bundleOf
24 import androidx.fragment.app.activityViewModels
25 import androidx.fragment.app.commitNow
26 import androidx.navigation.fragment.findNavController
27 import androidx.preference.Preference
28 import androidx.preference.PreferenceCategory
29 import com.android.healthconnect.controller.R
30 import com.android.healthconnect.controller.categories.HealthDataCategoriesFragment.Companion.CATEGORY_KEY
31 import com.android.healthconnect.controller.data.appdata.AppDataFragment.Companion.PERMISSION_TYPE_KEY
32 import com.android.healthconnect.controller.data.appdata.PermissionTypesPerCategory
33 import com.android.healthconnect.controller.permissions.data.FitnessPermissionStrings
34 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
35 import com.android.healthconnect.controller.selectabledeletion.DeletionConstants.START_DELETION_KEY
36 import com.android.healthconnect.controller.selectabledeletion.DeletionFragment
37 import com.android.healthconnect.controller.selectabledeletion.DeletionPermissionTypesPreference
38 import com.android.healthconnect.controller.selectabledeletion.DeletionViewModel
39 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.icon
40 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.uppercaseTitle
41 import com.android.healthconnect.controller.shared.HealthDataCategoryInt
42 import com.android.healthconnect.controller.shared.children
43 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
44 import com.android.healthconnect.controller.shared.preference.NoDataPreference
45 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
46 import com.android.healthconnect.controller.utils.setupMenu
47 import com.android.settingslib.widget.FooterPreference
48 import dagger.hilt.android.AndroidEntryPoint
49 import javax.inject.Inject
50 
51 /** Fragment for health permission types. */
52 @AndroidEntryPoint(HealthPreferenceFragment::class)
53 open class AllDataFragment : Hilt_AllDataFragment() {
54 
55     companion object {
56         private const val TAG = "AllDataFragmentTag"
57         private const val DELETION_TAG = "DeletionTag"
58     }
59 
60     @Inject lateinit var logger: HealthConnectLogger
61 
62     @HealthDataCategoryInt private var category: Int = 0
63 
64     private val viewModel: AllDataViewModel by activityViewModels()
65 
66     private val deletionViewModel: DeletionViewModel by activityViewModels()
67 
68     // Empty state
69     private val onDataSourcesClick: (MenuItem) -> Boolean = { menuItem ->
70         when (menuItem.itemId) {
71             R.id.menu_data_sources -> {
72                 findNavController()
73                     .navigate(
74                         R.id.action_allDataFragment_to_dataSourcesFragment,
75                         bundleOf(CATEGORY_KEY to category))
76                 true
77             }
78             else -> false
79         }
80     }
81 
82     // Not in deletion state
83     private val onMenuSetup: (MenuItem) -> Boolean = { menuItem ->
84         when (menuItem.itemId) {
85             R.id.menu_data_sources -> {
86                 findNavController()
87                     .navigate(
88                         R.id.action_allDataFragment_to_dataSourcesFragment,
89                         bundleOf(CATEGORY_KEY to category))
90                 true
91             }
92             R.id.menu_enter_deletion_state -> {
93                 // enter deletion state
94                 triggerDeletionState(true)
95                 true
96             }
97             else -> false
98         }
99     }
100 
101     // In deletion state with data selected
102     private val onEnterDeletionState: (MenuItem) -> Boolean = { menuItem ->
103         when (menuItem.itemId) {
104             R.id.delete -> {
105                 deleteData()
106                 true
107             }
108             else -> false
109         }
110     }
111 
112     // In deletion state without any data selected
113     private val onEmptyDeleteSetSetup: (MenuItem) -> Boolean = { menuItem ->
114         when (menuItem.itemId) {
115             R.id.menu_exit_deletion_state -> {
116                 // exit deletion state
117                 triggerDeletionState(false)
118                 true
119             }
120             else -> false
121         }
122     }
123 
124     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
125         super.onCreatePreferences(savedInstanceState, rootKey)
126         setPreferencesFromResource(R.xml.app_data_screen, rootKey)
127         if (childFragmentManager.findFragmentByTag(DELETION_TAG) == null) {
128             childFragmentManager.commitNow { add(DeletionFragment(), DELETION_TAG) }
129         }
130     }
131 
132     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
133         super.onViewCreated(view, savedInstanceState)
134 
135         viewModel.loadAllData()
136 
137         viewModel.allData.observe(viewLifecycleOwner) { state ->
138             when (state) {
139                 is AllDataViewModel.AllDataState.Loading -> {
140                     setLoading(isLoading = true)
141                     if (!viewModel.getDeletionState()) {
142                         updateMenu(isDeletionState = false)
143                         triggerDeletionState(isDeletionState = false)
144                     }
145                 }
146                 is AllDataViewModel.AllDataState.Error -> {
147                     setError(hasError = true)
148                 }
149                 is AllDataViewModel.AllDataState.WithData -> {
150                     setLoading(isLoading = false)
151                     setError(hasError = false)
152                     updatePreferenceScreen(state.dataMap)
153                 }
154             }
155         }
156 
157         deletionViewModel.permissionTypesReloadNeeded.observe(viewLifecycleOwner) { isReloadNeeded
158             ->
159             if (isReloadNeeded) {
160                 viewModel.setDeletionState(false)
161                 viewModel.loadAllData()
162                 deletionViewModel.resetPermissionTypesReloadNeeded()
163             }
164         }
165     }
166 
167     private fun updatePreferenceScreen(
168         permissionTypesPerCategoryList: List<PermissionTypesPerCategory>
169     ) {
170         preferenceScreen?.removeAll()
171 
172         val populatedCategories =
173             permissionTypesPerCategoryList
174                 .filter { it.data.isNotEmpty() }
175                 .sortedBy { getString(it.category.uppercaseTitle()) }
176 
177         if (populatedCategories.isEmpty()) {
178             setupEmptyState()
179             return
180         }
181 
182         setupMenu()
183 
184         populatedCategories.forEach { permissionTypesPerCategory ->
185             val category = permissionTypesPerCategory.category
186             val categoryIcon = category.icon(requireContext())
187 
188             val preferenceCategory =
189                 PreferenceCategory(requireContext()).also { it.setTitle(category.uppercaseTitle()) }
190             preferenceScreen.addPreference(preferenceCategory)
191 
192             permissionTypesPerCategory.data
193                 .sortedBy {
194                     getString(FitnessPermissionStrings.fromPermissionType(it).uppercaseLabel)
195                 }
196                 .forEach { permissionType ->
197                     preferenceCategory.addPreference(
198                         getPermissionTypePreference(permissionType, categoryIcon))
199                 }
200         }
201     }
202 
203     private fun onDeletionMethod(preference: DeletionPermissionTypesPreference): () -> Unit {
204         return {
205             if (preference.getHealthPermissionType() !in viewModel.getDeleteSet()) {
206                 viewModel.addToDeleteSet(preference.getHealthPermissionType())
207             } else {
208                 viewModel.removeFromDeleteSet(preference.getHealthPermissionType())
209             }
210             updateMenu(isDeletionState = true)
211         }
212     }
213 
214     private fun updateMenu(isDeletionState: Boolean, hasData: Boolean = true) {
215         if (!hasData) {
216             setupMenu(
217                 R.menu.all_data_empty_state_menu, viewLifecycleOwner, logger, onDataSourcesClick)
218             return
219         }
220 
221         if (!isDeletionState) {
222             setupMenu(R.menu.all_data_menu, viewLifecycleOwner, logger, onMenuSetup)
223             return
224         }
225 
226         if (viewModel.getDeleteSet().isEmpty()) {
227             setupMenu(
228                 R.menu.all_data_delete_menu, viewLifecycleOwner, logger, onEmptyDeleteSetSetup)
229             return
230         }
231 
232         setupMenu(R.menu.deletion_state_menu, viewLifecycleOwner, logger, onEnterDeletionState)
233     }
234 
235     @VisibleForTesting
236     fun triggerDeletionState(isDeletionState: Boolean) {
237         viewModel.setDeletionState(isDeletionState)
238 
239         preferenceScreen.children.forEach { preference ->
240             if (preference is PreferenceCategory) {
241                 preference.children.forEach { permissionTypePreference ->
242                     if (permissionTypePreference is DeletionPermissionTypesPreference) {
243                         permissionTypePreference.showCheckbox(isDeletionState)
244                     }
245                 }
246             }
247 
248             updateMenu(isDeletionState)
249         }
250     }
251 
252     private fun setupMenu() {
253         updateMenu(isDeletionState = viewModel.getDeletionState())
254     }
255 
256     private fun setupEmptyState() {
257         preferenceScreen.addPreference(NoDataPreference(requireContext()))
258         preferenceScreen.addPreference(
259             FooterPreference(requireContext()).also { it.setTitle(R.string.no_data_footer) })
260         updateMenu(isDeletionState = false, hasData = false)
261     }
262 
263     private fun deleteData() {
264         deletionViewModel.setDeleteSet(viewModel.getDeleteSet())
265         childFragmentManager.setFragmentResult(START_DELETION_KEY, bundleOf())
266     }
267 
268     private fun getPermissionTypePreference(
269         permissionType: HealthPermissionType,
270         categoryIcon: Drawable?
271     ): Preference {
272         return DeletionPermissionTypesPreference(requireContext()).also { preference ->
273             preference.setShowCheckbox(viewModel.getDeletionState())
274             if (permissionType in viewModel.getDeleteSet()) {
275                 preference.setIsChecked(true)
276             }
277 
278             preference.icon = categoryIcon
279             preference.setTitle(
280                 FitnessPermissionStrings.fromPermissionType(permissionType).uppercaseLabel)
281 
282             preference.setHealthPermissionType(permissionType)
283             // TODO(b/291249677): Add in upcoming CL.
284             // it.logName = AllDataElement.PERMISSION_TYPE_BUTTON
285 
286             preference.setOnPreferenceClickListener(onDeletionMethod(preference)) {
287                 findNavController()
288                     .navigate(
289                         R.id.action_allData_to_entriesAndAccess,
290                         bundleOf(PERMISSION_TYPE_KEY to permissionType))
291                 true
292             }
293         }
294     }
295 }
296