/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * */ /** * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * ``` * http://www.apache.org/licenses/LICENSE-2.0 * ``` * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.android.healthconnect.controller.permissions.connectedapps import android.content.Intent.EXTRA_PACKAGE_NAME import android.os.Bundle import android.view.View import androidx.annotation.StringRes import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import androidx.preference.Preference import androidx.preference.PreferenceGroup import com.android.healthconnect.controller.R import com.android.healthconnect.controller.migration.MigrationActivity.Companion.maybeShowWhatsNewDialog import com.android.healthconnect.controller.migration.MigrationActivity.Companion.showDataRestoreInProgressDialog import com.android.healthconnect.controller.migration.MigrationActivity.Companion.showMigrationInProgressDialog import com.android.healthconnect.controller.migration.MigrationViewModel import com.android.healthconnect.controller.migration.api.MigrationRestoreState import com.android.healthconnect.controller.migration.api.MigrationRestoreState.DataRestoreUiState import com.android.healthconnect.controller.migration.api.MigrationRestoreState.MigrationUiState import com.android.healthconnect.controller.permissions.app.SettingsFitnessAppFragment.Companion.EXTRA_IS_SYSTEM_APP import com.android.healthconnect.controller.shared.Constants.EXTRA_APP_NAME import com.android.healthconnect.controller.shared.app.AppPermissionsType import com.android.healthconnect.controller.shared.app.ConnectedAppMetadata import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.ALLOWED import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.DENIED import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment import com.android.healthconnect.controller.shared.preference.addIntroOrPermissionHeaderPreference import com.android.healthconnect.controller.utils.AttributeResolver import com.android.healthconnect.controller.utils.NavigationUtils import com.android.healthconnect.controller.utils.dismissLoadingDialog import com.android.healthconnect.controller.utils.logging.AppPermissionsElement import com.android.healthconnect.controller.utils.logging.HealthConnectLogger import com.android.healthconnect.controller.utils.logging.PageName import com.android.healthconnect.controller.utils.pref import com.android.healthconnect.controller.utils.setupMenu import com.android.healthconnect.controller.utils.showLoadingDialog import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject /** * Fragment to show allowed and denied apps for health permissions. It is used as an entry point * from PermissionController. */ @AndroidEntryPoint(HealthPreferenceFragment::class) class SettingsManagePermissionFragment : Hilt_SettingsManagePermissionFragment() { companion object { const val ALLOWED_APPS_GROUP = "allowed_apps" const val DENIED_APPS_GROUP = "denied_apps" } init { this.setPageName(PageName.SETTINGS_MANAGE_PERMISSIONS_PAGE) } @Inject lateinit var navigationUtils: NavigationUtils @Inject lateinit var logger: HealthConnectLogger private val allowedAppsGroup: PreferenceGroup by pref(ALLOWED_APPS_GROUP) private val deniedAppsGroup: PreferenceGroup by pref(DENIED_APPS_GROUP) private val viewModel: ConnectedAppsViewModel by viewModels() private val migrationViewModel: MigrationViewModel by viewModels() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { super.onCreatePreferences(savedInstanceState, rootKey) setPreferencesFromResource(R.xml.settings_manage_permission_screen, rootKey) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) addIntroOrPermissionHeaderPreference( preferenceScreen, requireContext(), getString(R.string.settings_manage_permission), AttributeResolver.getDrawable(requireContext(), R.attr.healthConnectIcon), getString(R.string.health_permission_header_description), ) viewModel.connectedApps.observe(viewLifecycleOwner) { connectedApps -> val connectedAppsGroup = connectedApps.groupBy { it.status } updateAllowedApps(connectedAppsGroup[ALLOWED].orEmpty()) updateDeniedApps(connectedAppsGroup[DENIED].orEmpty()) } viewModel.disconnectAllState.observe(viewLifecycleOwner) { state -> when (state) { is ConnectedAppsViewModel.DisconnectAllState.Loading -> { showLoadingDialog() } else -> { dismissLoadingDialog() } } } migrationViewModel.migrationState.observe(viewLifecycleOwner) { migrationState -> when (migrationState) { is MigrationViewModel.MigrationFragmentState.WithData -> { maybeShowMigrationDialog(migrationState.migrationRestoreState) } else -> { // do nothing } } } setupMenu(R.menu.show_system_with_send_feedback_and_help, viewLifecycleOwner, logger) { menuItem -> if (menuItem.itemId == R.id.menu_show_hide_system) { val isShowingSystem = viewModel.showSystemApps.value ?: false menuItem.setTitle( if (isShowingSystem) R.string.menu_show_system else R.string.menu_hide_system ) viewModel.setShowSystemApps(!isShowingSystem) true } else { false } } } private fun maybeShowMigrationDialog(migrationRestoreState: MigrationRestoreState) { val (migrationUiState, dataRestoreUiState, dataErrorState) = migrationRestoreState if (dataRestoreUiState == DataRestoreUiState.IN_PROGRESS) { showDataRestoreInProgressDialog(requireContext()) { _, _ -> requireActivity().finish() } } else if (migrationUiState == MigrationUiState.IN_PROGRESS) { showMigrationInProgressDialog( requireContext(), getString(R.string.migration_in_progress_permissions_dialog_content_apps), ) { _, _ -> requireActivity().finish() } } else if (migrationUiState == MigrationUiState.COMPLETE) { maybeShowWhatsNewDialog(requireContext()) } } override fun onResume() { super.onResume() viewModel.loadConnectedApps() } private fun updateAllowedApps(appsList: List) { allowedAppsGroup.removeAll() if (appsList.isEmpty()) { allowedAppsGroup.addPreference(getNoAppsPreference(R.string.no_apps_allowed)) } else { appsList.forEach { app -> allowedAppsGroup.addPreference(getAppPreference(app)) } } } private fun updateDeniedApps(appsList: List) { deniedAppsGroup.removeAll() if (appsList.isEmpty()) { deniedAppsGroup.addPreference(getNoAppsPreference(R.string.no_apps_denied)) } else { appsList.forEach { app -> deniedAppsGroup.addPreference(getAppPreference(app)) } } } private fun getNoAppsPreference(@StringRes res: Int): Preference { return Preference(requireContext()).also { it.setTitle(res) it.isSelectable = false } } private fun getAppPreference(app: ConnectedAppMetadata): HealthAppPreference { return HealthAppPreference(requireContext(), app.appMetadata).also { if (app.status == ALLOWED) { it.logName = AppPermissionsElement.CONNECTED_APP_BUTTON } else if (app.status == DENIED) { it.logName = AppPermissionsElement.NOT_CONNECTED_APP_BUTTON } if (app.healthUsageLastAccess != null) { it.setSummary(R.string.app_perms_content_provider_24h) } else { it.summary = null } it.setOnPreferenceClickListener { navigateToSettingsAppInfoScreen(app) true } } } private fun navigateToSettingsAppInfoScreen(app: ConnectedAppMetadata) { val navigationId = when (app.permissionsType) { AppPermissionsType.FITNESS_PERMISSIONS_ONLY -> R.id.action_settingsManagePermission_to_settingsFitnessApp AppPermissionsType.MEDICAL_PERMISSIONS_ONLY -> R.id.action_settingsManagePermission_to_settingsMedicalApp AppPermissionsType.COMBINED_PERMISSIONS -> R.id.action_settingsManagePermission_to_settingsCombinedPermissions } navigationUtils.navigate( fragment = this, action = navigationId, bundle = bundleOf( EXTRA_PACKAGE_NAME to app.appMetadata.packageName, EXTRA_APP_NAME to app.appMetadata.appName, EXTRA_IS_SYSTEM_APP to app.isSystem, ), ) } }