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