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.dataaccess
17 
18 import android.content.Intent.EXTRA_PACKAGE_NAME
19 import android.os.Bundle
20 import android.view.View
21 import androidx.core.os.bundleOf
22 import androidx.fragment.app.commitNow
23 import androidx.fragment.app.viewModels
24 import androidx.navigation.fragment.findNavController
25 import androidx.preference.Preference
26 import androidx.preference.PreferenceGroup
27 import com.android.healthconnect.controller.R
28 import com.android.healthconnect.controller.data.access.AccessViewModel
29 import com.android.healthconnect.controller.data.access.AccessViewModel.AccessScreenState
30 import com.android.healthconnect.controller.data.access.AppAccessState
31 import com.android.healthconnect.controller.deletion.DeletionConstants.DELETION_TYPE
32 import com.android.healthconnect.controller.deletion.DeletionConstants.FRAGMENT_TAG_DELETION
33 import com.android.healthconnect.controller.deletion.DeletionConstants.START_DELETION_EVENT
34 import com.android.healthconnect.controller.deletion.DeletionFragment
35 import com.android.healthconnect.controller.deletion.DeletionType
36 import com.android.healthconnect.controller.permissions.connectedapps.HealthAppPreference
37 import com.android.healthconnect.controller.permissions.data.FitnessPermissionStrings.Companion.fromPermissionType
38 import com.android.healthconnect.controller.permissions.data.HealthPermissionType
39 import com.android.healthconnect.controller.permissiontypes.HealthPermissionTypesFragment.Companion.PERMISSION_TYPE_KEY
40 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.fromHealthPermissionType
41 import com.android.healthconnect.controller.shared.HealthDataCategoryExtensions.icon
42 import com.android.healthconnect.controller.shared.app.AppMetadata
43 import com.android.healthconnect.controller.shared.inactiveapp.InactiveAppPreference
44 import com.android.healthconnect.controller.shared.preference.HealthPreference
45 import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
46 import com.android.healthconnect.controller.utils.logging.DataAccessElement
47 import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
48 import com.android.healthconnect.controller.utils.logging.PageName
49 import com.android.healthconnect.controller.utils.logging.ToolbarElement
50 import com.android.healthconnect.controller.utils.setTitle
51 import com.android.healthconnect.controller.utils.setupMenu
52 import com.android.settingslib.widget.AppHeaderPreference
53 import com.android.settingslib.widget.TopIntroPreference
54 import dagger.hilt.android.AndroidEntryPoint
55 import javax.inject.Inject
56 
57 /** Fragment displaying health data access information. */
58 @AndroidEntryPoint(HealthPreferenceFragment::class)
59 class HealthDataAccessFragment : Hilt_HealthDataAccessFragment() {
60 
61     companion object {
62         private const val DATA_ACCESS_HEADER = "data_access_header"
63         private const val PERMISSION_TYPE_DESCRIPTION = "permission_type_description"
64         private const val CAN_READ_SECTION = "can_read"
65         private const val CAN_WRITE_SECTION = "can_write"
66         private const val INACTIVE_SECTION = "inactive"
67         private const val ALL_ENTRIES_BUTTON = "all_entries_button"
68         private const val DELETE_PERMISSION_TYPE_DATA_BUTTON = "delete_permission_type_data"
69     }
70 
71     init {
72         this.setPageName(PageName.DATA_ACCESS_PAGE)
73     }
74 
75     @Inject lateinit var logger: HealthConnectLogger
76 
77     private val viewModel: AccessViewModel by viewModels()
78 
79     private lateinit var permissionType: HealthPermissionType
80 
81     private val mDataAccessHeader: AppHeaderPreference? by lazy {
82         preferenceScreen.findPreference(DATA_ACCESS_HEADER)
83     }
84 
85     private val mPermissionTypeDescription: TopIntroPreference? by lazy {
86         preferenceScreen.findPreference(PERMISSION_TYPE_DESCRIPTION)
87     }
88 
89     private val mCanReadSection: PreferenceGroup? by lazy {
90         preferenceScreen.findPreference(CAN_READ_SECTION)
91     }
92 
93     private val mCanWriteSection: PreferenceGroup? by lazy {
94         preferenceScreen.findPreference(CAN_WRITE_SECTION)
95     }
96 
97     private val mInactiveSection: PreferenceGroup? by lazy {
98         preferenceScreen.findPreference(INACTIVE_SECTION)
99     }
100 
101     private val mAllEntriesButton: HealthPreference? by lazy {
102         preferenceScreen.findPreference(ALL_ENTRIES_BUTTON)
103     }
104 
105     private val mDeletePermissionTypeData: HealthPreference? by lazy {
106         preferenceScreen.findPreference(DELETE_PERMISSION_TYPE_DATA_BUTTON)
107     }
108 
109     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
110         super.onCreatePreferences(savedInstanceState, rootKey)
111         setPreferencesFromResource(R.xml.health_data_access_screen, rootKey)
112         if (requireArguments().containsKey(PERMISSION_TYPE_KEY)) {
113             permissionType =
114                 arguments?.getSerializable(PERMISSION_TYPE_KEY, HealthPermissionType::class.java)
115                     ?: throw IllegalArgumentException("PERMISSION_TYPE_KEY can't be null!")
116         }
117 
118         mCanReadSection?.isVisible = false
119         mCanWriteSection?.isVisible = false
120         mInactiveSection?.isVisible = false
121         maybeShowPermissionTypeDescription()
122         mCanReadSection?.title =
123             getString(
124                 R.string.can_read, getString(fromPermissionType(permissionType).lowercaseLabel))
125         mCanWriteSection?.title =
126             getString(
127                 R.string.can_write, getString(fromPermissionType(permissionType).lowercaseLabel))
128         if (childFragmentManager.findFragmentByTag(FRAGMENT_TAG_DELETION) == null) {
129             childFragmentManager.commitNow { add(DeletionFragment(), FRAGMENT_TAG_DELETION) }
130         }
131 
132         mAllEntriesButton?.logName = DataAccessElement.SEE_ALL_ENTRIES_BUTTON
133         mAllEntriesButton?.setOnPreferenceClickListener {
134             findNavController()
135                 .navigate(
136                     R.id.action_healthDataAccess_to_dataEntries,
137                     bundleOf(PERMISSION_TYPE_KEY to permissionType))
138             true
139         }
140         mDeletePermissionTypeData?.logName = DataAccessElement.DELETE_THIS_DATA_BUTTON
141         mDeletePermissionTypeData?.setOnPreferenceClickListener {
142             val deletionType =
143                 DeletionType.DeletionTypeHealthPermissionTypeData(
144                     healthPermissionType = permissionType)
145             childFragmentManager.setFragmentResult(
146                 START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionType))
147             true
148         }
149     }
150 
151     private fun maybeShowPermissionTypeDescription() {
152         mPermissionTypeDescription?.isVisible = false
153         if (permissionType == HealthPermissionType.EXERCISE) {
154             mPermissionTypeDescription?.isVisible = true
155             mPermissionTypeDescription?.setTitle(R.string.data_access_exercise_description)
156         }
157         if (permissionType == HealthPermissionType.SLEEP) {
158             mPermissionTypeDescription?.isVisible = true
159             mPermissionTypeDescription?.setTitle(R.string.data_access_sleep_description)
160         }
161     }
162 
163     override fun onResume() {
164         super.onResume()
165         setTitle(fromPermissionType(permissionType).uppercaseLabel)
166         viewModel.loadAppMetaDataMap(permissionType)
167     }
168 
169     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
170         super.onViewCreated(view, savedInstanceState)
171 
172         mDataAccessHeader?.icon = fromHealthPermissionType(permissionType).icon(requireContext())
173         mDataAccessHeader?.title = getString(fromPermissionType(permissionType).uppercaseLabel)
174         viewModel.loadAppMetaDataMap(permissionType)
175         viewModel.appMetadataMap.observe(viewLifecycleOwner) { state ->
176             when (state) {
177                 is AccessScreenState.Loading -> {
178                     setLoading(isLoading = true)
179                 }
180                 is AccessScreenState.Error -> {
181                     setError(hasError = true)
182                 }
183                 is AccessScreenState.WithData -> {
184                     setLoading(isLoading = false, animate = false)
185                     updateDataAccess(state.appMetadata)
186                 }
187             }
188         }
189 
190         setupMenu(R.menu.set_data_units_with_send_feedback_and_help, viewLifecycleOwner, logger) {
191             menuItem ->
192             when (menuItem.itemId) {
193                 R.id.menu_open_units -> {
194                     logger.logImpression(ToolbarElement.TOOLBAR_UNITS_BUTTON)
195                     findNavController()
196                         .navigate(R.id.action_healthDataAccessFragment_to_unitsFragment)
197                     true
198                 }
199                 else -> false
200             }
201         }
202     }
203 
204     private fun updateDataAccess(appMetadataMap: Map<AppAccessState, List<AppMetadata>>) {
205         mCanReadSection?.removeAll()
206         mCanWriteSection?.removeAll()
207         mInactiveSection?.removeAll()
208 
209         if (appMetadataMap.containsKey(AppAccessState.Read)) {
210             if (appMetadataMap[AppAccessState.Read]!!.isEmpty()) {
211                 mCanReadSection?.isVisible = false
212             } else {
213                 mCanReadSection?.isVisible = true
214                 appMetadataMap[AppAccessState.Read]!!.forEach { _appMetadata ->
215                     mCanReadSection?.addPreference(createAppPreference(_appMetadata))
216                 }
217             }
218         }
219         if (appMetadataMap.containsKey(AppAccessState.Write)) {
220             if (appMetadataMap[AppAccessState.Write]!!.isEmpty()) {
221                 mCanWriteSection?.isVisible = false
222             } else {
223                 mCanWriteSection?.isVisible = true
224                 appMetadataMap[AppAccessState.Write]!!.forEach { _appMetadata ->
225                     mCanWriteSection?.addPreference(createAppPreference(_appMetadata))
226                 }
227             }
228         }
229         if (appMetadataMap.containsKey(AppAccessState.Inactive)) {
230             if (appMetadataMap[AppAccessState.Inactive]!!.isEmpty()) {
231                 mInactiveSection?.isVisible = false
232             } else {
233                 mInactiveSection?.isVisible = true
234                 mInactiveSection?.addPreference(
235                     Preference(requireContext()).also {
236                         it.summary =
237                             getString(
238                                 R.string.inactive_apps_message,
239                                 getString(fromPermissionType(permissionType).lowercaseLabel))
240                     })
241                 appMetadataMap[AppAccessState.Inactive]?.forEach { _appMetadata ->
242                     mInactiveSection?.addPreference(
243                         InactiveAppPreference(requireContext()).also {
244                             it.title = _appMetadata.appName
245                             it.icon = _appMetadata.icon
246                             it.logName = DataAccessElement.DATA_ACCESS_INACTIVE_APP_BUTTON
247                             it.setOnDeleteButtonClickListener {
248                                 val deletionType =
249                                     DeletionType.DeletionTypeAppData(
250                                         _appMetadata.packageName, _appMetadata.appName)
251                                 childFragmentManager.setFragmentResult(
252                                     START_DELETION_EVENT, bundleOf(DELETION_TYPE to deletionType))
253                             }
254                         })
255                 }
256             }
257         }
258     }
259 
260     private fun createAppPreference(appMetadata: AppMetadata): HealthAppPreference {
261         return HealthAppPreference(requireContext(), appMetadata).also {
262             it.logName = DataAccessElement.DATA_ACCESS_APP_BUTTON
263             it.setOnPreferenceClickListener {
264                 // TODO (b/270859815) might need to navigate to appAccess instead
265                 findNavController()
266                     .navigate(
267                         R.id.action_healthDataAccessFragment_to_appAccess,
268                         bundleOf(EXTRA_PACKAGE_NAME to appMetadata.packageName))
269                 true
270             }
271         }
272     }
273 }
274