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