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.permissiontypes 17 18 import android.health.connect.HealthDataCategory 19 import android.os.Bundle 20 import android.util.Log 21 import android.view.View 22 import android.widget.RadioGroup 23 import android.widget.Toast 24 import androidx.core.os.bundleOf 25 import androidx.fragment.app.activityViewModels 26 import androidx.fragment.app.commitNow 27 import androidx.navigation.fragment.findNavController 28 import androidx.preference.Preference 29 import androidx.preference.PreferenceGroup 30 import com.android.healthconnect.controller.R 31 import com.android.healthconnect.controller.categories.HealthDataCategoriesFragment.Companion.CATEGORY_KEY 32 import com.android.healthconnect.controller.deletion.DeletionConstants.DELETION_TYPE 33 import com.android.healthconnect.controller.deletion.DeletionConstants.FRAGMENT_TAG_DELETION 34 import com.android.healthconnect.controller.deletion.DeletionConstants.START_DELETION_EVENT 35 import com.android.healthconnect.controller.deletion.DeletionFragment 36 import com.android.healthconnect.controller.deletion.DeletionType 37 import com.android.healthconnect.controller.filters.ChipPreference 38 import com.android.healthconnect.controller.filters.FilterChip 39 import com.android.healthconnect.controller.permissions.data.FitnessPermissionStrings.Companion.fromPermissionType 40 import com.android.healthconnect.controller.permissions.data.HealthPermissionType 41 import com.android.healthconnect.controller.permissiontypes.prioritylist.PriorityListDialogFragment 42 import com.android.healthconnect.controller.permissiontypes.prioritylist.PriorityListDialogFragment.Companion.PRIORITY_UPDATED_EVENT 43 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.icon 44 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.lowercaseTitle 45 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.uppercaseTitle 46 import com.android.healthconnect.controller.shared.HealthDataCategoryInt 47 import com.android.healthconnect.controller.shared.app.AppMetadata 48 import com.android.healthconnect.controller.shared.preference.HealthPreference 49 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment 50 import com.android.healthconnect.controller.utils.AttributeResolver 51 import com.android.healthconnect.controller.utils.FeatureUtils 52 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger 53 import com.android.healthconnect.controller.utils.logging.PageName 54 import com.android.healthconnect.controller.utils.logging.PermissionTypesElement 55 import com.android.healthconnect.controller.utils.logging.ToolbarElement 56 import com.android.healthconnect.controller.utils.setupMenu 57 import com.android.settingslib.widget.AppHeaderPreference 58 import dagger.hilt.android.AndroidEntryPoint 59 import javax.inject.Inject 60 61 /** Fragment for health permission types. */ 62 @AndroidEntryPoint(HealthPreferenceFragment::class) 63 open class HealthPermissionTypesFragment : Hilt_HealthPermissionTypesFragment() { 64 65 companion object { 66 private const val TAG = "HealthPermissionTypesFT" 67 private const val PERMISSION_TYPES_HEADER = "permission_types_header" 68 private const val APP_FILTERS_PREFERENCE = "app_filters_preference" 69 private const val PERMISSION_TYPES_CATEGORY = "permission_types" 70 private const val MANAGE_DATA_CATEGORY = "manage_data_category" 71 const val PERMISSION_TYPE_KEY = "permission_type_key" 72 private const val APP_PRIORITY_BUTTON = "app_priority" 73 private const val DELETE_CATEGORY_DATA_BUTTON = "delete_category_data" 74 } 75 76 init { 77 this.setPageName(PageName.PERMISSION_TYPES_PAGE) 78 } 79 80 @Inject lateinit var logger: HealthConnectLogger 81 @Inject lateinit var featureUtils: FeatureUtils 82 83 @HealthDataCategoryInt private var category: Int = 0 84 85 private val viewModel: HealthPermissionTypesViewModel by activityViewModels() 86 87 private val mPermissionTypesHeader: AppHeaderPreference? by lazy { 88 preferenceScreen.findPreference(PERMISSION_TYPES_HEADER) 89 } 90 91 private val mAppFiltersPreference: PreferenceGroup? by lazy { 92 preferenceScreen.findPreference(APP_FILTERS_PREFERENCE) 93 } 94 95 private val mPermissionTypes: PreferenceGroup? by lazy { 96 preferenceScreen.findPreference(PERMISSION_TYPES_CATEGORY) 97 } 98 99 private val mManageDataCategory: PreferenceGroup? by lazy { 100 preferenceScreen.findPreference(MANAGE_DATA_CATEGORY) 101 } 102 103 private val mDeleteCategoryData: HealthPreference? by lazy { 104 preferenceScreen.findPreference(DELETE_CATEGORY_DATA_BUTTON) 105 } 106 107 override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 108 super.onCreatePreferences(savedInstanceState, rootKey) 109 setPreferencesFromResource(R.xml.health_permission_types_screen, rootKey) 110 111 if (requireArguments().containsKey(CATEGORY_KEY)) { 112 category = requireArguments().getInt(CATEGORY_KEY) 113 } 114 115 if (childFragmentManager.findFragmentByTag(FRAGMENT_TAG_DELETION) == null) { 116 childFragmentManager.commitNow { add(DeletionFragment(), FRAGMENT_TAG_DELETION) } 117 } 118 119 mDeleteCategoryData?.logName = PermissionTypesElement.DELETE_CATEGORY_DATA_BUTTON 120 mDeleteCategoryData?.title = 121 getString(R.string.delete_category_data_button, getString(category.lowercaseTitle())) 122 mDeleteCategoryData?.setOnPreferenceClickListener { 123 val deletionType = DeletionType.DeletionTypeCategoryData(category = category) 124 childFragmentManager.setFragmentResult( 125 START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionType)) 126 true 127 } 128 } 129 130 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 131 super.onViewCreated(view, savedInstanceState) 132 133 mPermissionTypesHeader?.icon = category.icon(requireContext()) 134 mPermissionTypesHeader?.title = getString(category.uppercaseTitle()) 135 viewModel.loadData(category) 136 viewModel.loadAppsWithData(category) 137 138 setupMenu(R.menu.set_data_units_with_send_feedback_and_help, viewLifecycleOwner, logger) { 139 menuItem -> 140 when (menuItem.itemId) { 141 R.id.menu_open_units -> { 142 logger.logImpression(ToolbarElement.TOOLBAR_UNITS_BUTTON) 143 findNavController().navigate(R.id.action_healthPermissionTypes_to_unitsFragment) 144 true 145 } 146 else -> false 147 } 148 } 149 150 viewModel.permissionTypesData.observe(viewLifecycleOwner) { state -> 151 when (state) { 152 is HealthPermissionTypesViewModel.PermissionTypesState.Loading -> {} 153 is HealthPermissionTypesViewModel.PermissionTypesState.WithData -> { 154 updatePermissionTypesList(state.permissionTypes) 155 } 156 } 157 } 158 viewModel.appsWithData.observe(viewLifecycleOwner) { state -> 159 when (state) { 160 is HealthPermissionTypesViewModel.AppsWithDataFragmentState.Loading -> {} 161 is HealthPermissionTypesViewModel.AppsWithDataFragmentState.WithData -> { 162 if (state.appsWithData.size > 1) { 163 addAppFilters(state.appsWithData) 164 mAppFiltersPreference?.isVisible = true 165 } else { 166 mAppFiltersPreference?.isVisible = false 167 } 168 } 169 } 170 } 171 childFragmentManager.setFragmentResultListener(PRIORITY_UPDATED_EVENT, this) { _, bundle -> 172 bundle.getStringArrayList(PRIORITY_UPDATED_EVENT)?.let { 173 try { 174 viewModel.updatePriorityList(category, it) 175 } catch (ex: Exception) { 176 Log.e(TAG, "Failed to update priorities!", ex) 177 Toast.makeText(requireContext(), R.string.default_error, Toast.LENGTH_SHORT) 178 .show() 179 } 180 } 181 } 182 183 if (!featureUtils.isNewAppPriorityEnabled()) { 184 viewModel.priorityList.observe(viewLifecycleOwner) { state -> 185 when (state) { 186 is HealthPermissionTypesViewModel.PriorityListState.Loading -> { 187 mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON) 188 } 189 is HealthPermissionTypesViewModel.PriorityListState.LoadingFailed -> { 190 mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON) 191 } 192 is HealthPermissionTypesViewModel.PriorityListState.WithData -> { 193 updateOldPriorityButton(state.priorityList) 194 } 195 } 196 } 197 } else { 198 // Add the new priority list button 199 updateNewPriorityButton() 200 } 201 } 202 203 private fun updateNewPriorityButton() { 204 mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON) 205 206 // Only display the priority button for Activity and Sleep categories 207 if (category !in setOf(HealthDataCategory.ACTIVITY, HealthDataCategory.SLEEP)) { 208 return 209 } 210 211 val newPriorityButton = 212 HealthPreference(requireContext()).also { 213 it.title = resources.getString(R.string.data_sources_and_priority_title) 214 it.icon = AttributeResolver.getDrawable(requireContext(), R.attr.appPriorityIcon) 215 it.logName = PermissionTypesElement.DATA_SOURCES_AND_PRIORITY_BUTTON 216 it.key = APP_PRIORITY_BUTTON 217 it.order = 4 218 it.setOnPreferenceClickListener { 219 // Navigate to the data sources fragment 220 findNavController() 221 .navigate( 222 R.id.action_healthPermissionTypes_to_dataSourcesAndPriority, 223 bundleOf(CATEGORY_KEY to category)) 224 true 225 } 226 } 227 228 mManageDataCategory?.addPreference(newPriorityButton) 229 } 230 231 private fun updateOldPriorityButton(priorityList: List<AppMetadata>) { 232 mManageDataCategory?.removePreferenceRecursively(APP_PRIORITY_BUTTON) 233 234 // Only display the priority button for Activity and Sleep categories 235 if (category !in setOf(HealthDataCategory.ACTIVITY, HealthDataCategory.SLEEP)) { 236 return 237 } 238 239 if (priorityList.size < 2) { 240 return 241 } 242 243 val appPriorityButton = 244 HealthPreference(requireContext()).also { 245 it.title = resources.getString(R.string.app_priority_button) 246 it.icon = AttributeResolver.getDrawable(requireContext(), R.attr.appPriorityIcon) 247 it.logName = PermissionTypesElement.SET_APP_PRIORITY_BUTTON 248 it.summary = priorityList.first().appName 249 it.key = APP_PRIORITY_BUTTON 250 it.order = 4 251 it.setOnPreferenceClickListener { 252 viewModel.setEditedPriorityList(priorityList) 253 viewModel.setCategoryLabel(getString(category.lowercaseTitle())) 254 PriorityListDialogFragment() 255 .show(childFragmentManager, PriorityListDialogFragment.TAG) 256 true 257 } 258 } 259 mManageDataCategory?.addPreference(appPriorityButton) 260 } 261 262 private fun updatePermissionTypesList(permissionTypeList: List<HealthPermissionType>) { 263 mDeleteCategoryData?.isEnabled = permissionTypeList.isNotEmpty() 264 mPermissionTypes?.removeAll() 265 if (permissionTypeList.isEmpty()) { 266 mPermissionTypes?.addPreference( 267 Preference(requireContext()).also { it.setSummary(R.string.no_categories) }) 268 return 269 } 270 permissionTypeList.forEach { permissionType -> 271 mPermissionTypes?.addPreference( 272 HealthPreference(requireContext()).also { 273 it.setTitle(fromPermissionType(permissionType).uppercaseLabel) 274 it.logName = PermissionTypesElement.PERMISSION_TYPE_BUTTON 275 it.setOnPreferenceClickListener { 276 findNavController() 277 .navigate( 278 R.id.action_healthPermissionTypes_to_healthDataAccess, 279 bundleOf(PERMISSION_TYPE_KEY to permissionType)) 280 true 281 } 282 }) 283 } 284 } 285 286 private fun addAppFilters(appsWithHealthPermissions: List<AppMetadata>) { 287 mAppFiltersPreference?.removeAll() 288 mAppFiltersPreference?.addPreference( 289 ChipPreference( 290 requireContext(), 291 appsWithHealthPermissions, 292 addFilterChip = ::addFilterChip, 293 addAllAppsFilterChip = ::addAllAppsFilterChip)) 294 } 295 296 private fun addFilterChip(appMetadata: AppMetadata, chipGroup: RadioGroup) { 297 val newFilterChip = FilterChip(requireContext()) 298 newFilterChip.id = View.generateViewId() 299 newFilterChip.setUnselectedIcon(appMetadata.icon) 300 newFilterChip.text = appMetadata.appName 301 if (appMetadata.appName == viewModel.selectedAppFilter.value) { 302 newFilterChip.isChecked = true 303 viewModel.filterPermissionTypes(category, appMetadata.packageName) 304 } 305 newFilterChip.setOnClickListener { 306 viewModel.setAppFilter(appMetadata.appName) 307 viewModel.filterPermissionTypes(category, appMetadata.packageName) 308 } 309 chipGroup.addView(newFilterChip) 310 } 311 312 private fun addAllAppsFilterChip(chipGroup: RadioGroup) { 313 val allAppsButton = FilterChip(requireContext()) 314 val selectAllAppsTitle = resources.getString(R.string.select_all_apps_title) 315 allAppsButton.id = R.id.select_all_chip 316 allAppsButton.setUnselectedIcon(null) 317 allAppsButton.text = requireContext().resources.getString(R.string.select_all_apps_title) 318 if (viewModel.selectedAppFilter.value == selectAllAppsTitle) { 319 allAppsButton.isChecked = true 320 viewModel.filterPermissionTypes(category, selectAllAppsTitle) 321 } 322 323 allAppsButton.setOnClickListener { 324 viewModel.setAppFilter(selectAllAppsTitle) 325 viewModel.filterPermissionTypes(category, selectAllAppsTitle) 326 } 327 chipGroup.addView(allAppsButton) 328 } 329 } 330