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