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