1 /**
<lambda>null2  * Copyright (C) 2022 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.categories
17 
18 import android.icu.text.MessageFormat
19 import android.os.Bundle
20 import android.view.View
21 import androidx.annotation.AttrRes
22 import androidx.core.os.bundleOf
23 import androidx.fragment.app.activityViewModels
24 import androidx.fragment.app.commitNow
25 import androidx.fragment.app.viewModels
26 import androidx.navigation.fragment.findNavController
27 import androidx.preference.Preference
28 import androidx.preference.PreferenceGroup
29 import com.android.healthconnect.controller.R
30 import com.android.healthconnect.controller.autodelete.AutoDeleteRange
31 import com.android.healthconnect.controller.autodelete.AutoDeleteViewModel
32 import com.android.healthconnect.controller.categories.HealthDataCategoryViewModel.CategoriesFragmentState.Error
33 import com.android.healthconnect.controller.categories.HealthDataCategoryViewModel.CategoriesFragmentState.Loading
34 import com.android.healthconnect.controller.categories.HealthDataCategoryViewModel.CategoriesFragmentState.WithData
35 import com.android.healthconnect.controller.deletion.DeletionConstants.DELETION_TYPE
36 import com.android.healthconnect.controller.deletion.DeletionConstants.FRAGMENT_TAG_DELETION
37 import com.android.healthconnect.controller.deletion.DeletionConstants.START_DELETION_EVENT
38 import com.android.healthconnect.controller.deletion.DeletionFragment
39 import com.android.healthconnect.controller.deletion.DeletionType
40 import com.android.healthconnect.controller.deletion.DeletionViewModel
41 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.icon
42 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.uppercaseTitle
43 import com.android.healthconnect.controller.shared.preference.HealthPreference
44 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
45 import com.android.healthconnect.controller.utils.AttributeResolver
46 import com.android.healthconnect.controller.utils.FeatureUtils
47 import com.android.healthconnect.controller.utils.logging.CategoriesElement
48 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
49 import com.android.healthconnect.controller.utils.logging.PageName
50 import com.android.healthconnect.controller.utils.logging.ToolbarElement
51 import com.android.healthconnect.controller.utils.setupMenu
52 import dagger.hilt.android.AndroidEntryPoint
53 import javax.inject.Inject
54 
55 /** Fragment for health data categories. */
56 @AndroidEntryPoint(HealthPreferenceFragment::class)
57 class HealthDataCategoriesFragment : Hilt_HealthDataCategoriesFragment() {
58 
59     companion object {
60         const val CATEGORY_KEY = "category_key"
61         private const val BROWSE_DATA_CATEGORY = "browse_data_category"
62         private const val AUTO_DELETE_BUTTON = "auto_delete_button"
63         private const val DELETE_ALL_DATA_BUTTON = "delete_all_data"
64     }
65 
66     init {
67         this.setPageName(PageName.CATEGORIES_PAGE)
68     }
69 
70     @Inject lateinit var logger: HealthConnectLogger
71     @Inject lateinit var featureUtils: FeatureUtils
72 
73     private val categoriesViewModel: HealthDataCategoryViewModel by viewModels()
74     private val autoDeleteViewModel: AutoDeleteViewModel by activityViewModels()
75     private val deletionViewModel: DeletionViewModel by activityViewModels()
76 
77     private val mBrowseDataCategory: PreferenceGroup? by lazy {
78         preferenceScreen.findPreference(BROWSE_DATA_CATEGORY)
79     }
80     private val mAutoDelete: HealthPreference? by lazy {
81         preferenceScreen.findPreference(AUTO_DELETE_BUTTON)
82     }
83     private val mDeleteAllData: HealthPreference? by lazy {
84         preferenceScreen.findPreference(DELETE_ALL_DATA_BUTTON)
85     }
86 
87     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
88         super.onCreatePreferences(savedInstanceState, rootKey)
89         setPreferencesFromResource(R.xml.health_data_categories_screen, rootKey)
90 
91         if (childFragmentManager.findFragmentByTag(FRAGMENT_TAG_DELETION) == null) {
92             childFragmentManager.commitNow { add(DeletionFragment(), FRAGMENT_TAG_DELETION) }
93         }
94 
95         if (!featureUtils.isNewAppPriorityEnabled()) {
96             mAutoDelete?.isVisible = true
97             mAutoDelete?.logName = CategoriesElement.AUTO_DELETE_BUTTON
98             mAutoDelete?.setOnPreferenceClickListener {
99                 findNavController().navigate(R.id.action_healthDataCategories_to_autoDelete)
100                 true
101             }
102         }
103 
104         mDeleteAllData?.logName = CategoriesElement.DELETE_ALL_DATA_BUTTON
105         mDeleteAllData?.setOnPreferenceClickListener {
106             val deletionType = DeletionType.DeletionTypeAllData()
107             childFragmentManager.setFragmentResult(
108                 START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionType))
109             true
110         }
111         mDeleteAllData?.isEnabled = false
112     }
113 
114     private fun buildSummary(autoDeleteRange: AutoDeleteRange): String {
115         return when (autoDeleteRange) {
116             AutoDeleteRange.AUTO_DELETE_RANGE_NEVER -> getString(R.string.range_off)
117             AutoDeleteRange.AUTO_DELETE_RANGE_THREE_MONTHS -> {
118                 val count = AutoDeleteRange.AUTO_DELETE_RANGE_THREE_MONTHS.numberOfMonths
119                 MessageFormat.format(
120                     getString(R.string.range_after_x_months), mapOf("count" to count))
121             }
122             AutoDeleteRange.AUTO_DELETE_RANGE_EIGHTEEN_MONTHS -> {
123                 val count = AutoDeleteRange.AUTO_DELETE_RANGE_EIGHTEEN_MONTHS.numberOfMonths
124                 MessageFormat.format(
125                     getString(R.string.range_after_x_months), mapOf("count" to count))
126             }
127         }
128     }
129 
130     @AttrRes
131     private fun iconAttribute(autoDeleteRange: AutoDeleteRange): Int {
132         return when (autoDeleteRange) {
133             AutoDeleteRange.AUTO_DELETE_RANGE_NEVER -> R.attr.autoDeleteOffIcon
134             else -> R.attr.autoDeleteIcon
135         }
136     }
137 
138     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
139         super.onViewCreated(view, savedInstanceState)
140         categoriesViewModel.loadCategories()
141 
142         categoriesViewModel.categoriesData.observe(viewLifecycleOwner) { state ->
143             when (state) {
144                 is Loading -> {
145                     setLoading(true)
146                 }
147                 is WithData -> {
148                     setLoading(false)
149                     updateDataList(state.categories)
150                 }
151                 Error -> {
152                     setError(true)
153                 }
154             }
155         }
156 
157         setupMenu(R.menu.set_data_units_with_send_feedback_and_help, viewLifecycleOwner, logger) {
158             menuItem ->
159             when (menuItem.itemId) {
160                 R.id.menu_open_units -> {
161                     logger.logImpression(ToolbarElement.TOOLBAR_UNITS_BUTTON)
162                     findNavController()
163                         .navigate(R.id.action_dataCategoriesFragment_to_unitsFragment)
164                     true
165                 }
166                 else -> false
167             }
168         }
169 
170         if (!featureUtils.isNewAppPriorityEnabled()) {
171             autoDeleteViewModel.storedAutoDeleteRange.observe(viewLifecycleOwner) { state ->
172                 when (state) {
173                     AutoDeleteViewModel.AutoDeleteState.Loading -> {
174                         mAutoDelete?.summary = ""
175                         mAutoDelete?.icon = null
176                     }
177                     is AutoDeleteViewModel.AutoDeleteState.LoadingFailed -> {
178                         mAutoDelete?.summary = ""
179                         mAutoDelete?.icon = null
180                     }
181                     is AutoDeleteViewModel.AutoDeleteState.WithData -> {
182                         mAutoDelete?.summary = buildSummary(state.autoDeleteRange)
183                         mAutoDelete?.setIcon(
184                             AttributeResolver.getResource(
185                                 requireContext(), iconAttribute(state.autoDeleteRange)))
186                     }
187                 }
188             }
189         }
190 
191         deletionViewModel.categoriesReloadNeeded.observe(viewLifecycleOwner) { isReloadNeeded ->
192             if (isReloadNeeded) {
193                 categoriesViewModel.loadCategories()
194             }
195         }
196     }
197 
198     private fun updateDataList(categoriesList: List<HealthCategoryUiState>) {
199         val sortedCategoriesList: List<HealthCategoryUiState> =
200             categoriesList
201                 .filter { it.hasData }
202                 .sortedBy { getString(it.category.uppercaseTitle()) }
203         mBrowseDataCategory?.removeAll()
204         if (sortedCategoriesList.isEmpty()) {
205             mBrowseDataCategory?.addPreference(
206                 Preference(requireContext()).also { it.setSummary(R.string.no_categories) })
207         } else {
208             sortedCategoriesList.forEach { categoryState ->
209                 val newCategoryPreference =
210                     HealthPreference(requireContext()).also {
211                         it.setTitle(categoryState.category.uppercaseTitle())
212                         it.icon = categoryState.category.icon(requireContext())
213                         it.logName = CategoriesElement.CATEGORY_BUTTON
214                         it.setOnPreferenceClickListener {
215                             findNavController()
216                                 .navigate(
217                                     R.id.action_healthDataCategories_to_healthPermissionTypes,
218                                     bundleOf(CATEGORY_KEY to categoryState.category))
219                             true
220                         }
221                     }
222                 mBrowseDataCategory?.addPreference(newCategoryPreference)
223             }
224         }
225         if (sortedCategoriesList.isEmpty() || categoriesList.any { !it.hasData }) {
226             addSeeAllCategoriesPreference()
227         }
228         mDeleteAllData?.isEnabled = sortedCategoriesList.isNotEmpty()
229     }
230 
231     private fun addSeeAllCategoriesPreference() {
232         mBrowseDataCategory?.addPreference(
233             HealthPreference(requireContext()).also {
234                 it.setTitle(R.string.see_all_categories)
235                 it.setIcon(AttributeResolver.getResource(requireContext(), R.attr.seeAllIcon))
236                 it.logName = CategoriesElement.SEE_ALL_CATEGORIES_BUTTON
237                 it.setOnPreferenceClickListener {
238                     findNavController()
239                         .navigate(R.id.action_healthDataCategories_to_healthDataAllCategories)
240                     true
241                 }
242             })
243     }
244 }
245