/* * Copyright (C) 2025 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.permissioncontroller.appfunctions.ui import android.content.Context import android.content.Intent import android.graphics.drawable.Drawable import android.os.Bundle import android.util.Log import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle.State import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceGroup import androidx.preference.PreferenceScreen import androidx.preference.TwoStatePreference import com.android.permissioncontroller.R import com.android.permissioncontroller.appfunctions.ui.viewmodel.AgentAccessUiState import com.android.permissioncontroller.appfunctions.ui.viewmodel.AgentAccessViewModel import com.android.permissioncontroller.appfunctions.ui.viewmodel.AgentAccessViewModelFactory import com.android.permissioncontroller.appfunctions.ui.viewmodel.DeviceSettingsItem import com.android.permissioncontroller.appfunctions.ui.viewmodel.TargetItem import com.android.permissioncontroller.common.model.Stateful import kotlinx.coroutines.launch /** * Child fragment for modifying agent access of target apps. * *

* Must be added as a child fragment and its parent fragment must be a {@link * PreferenceFragmentCompat} that implements {@link Parent}. * * @param type of the parent fragment */ class AgentAccessChildFragment : Fragment(), Preference.OnPreferenceClickListener where PF : PreferenceFragmentCompat, PF : AgentAccessChildFragment.Parent { private lateinit var agentPackageName: String private lateinit var viewModel: AgentAccessViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) agentPackageName = arguments!!.getString(Intent.EXTRA_PACKAGE_NAME)!! val factory = AgentAccessViewModelFactory(requireActivity().application, agentPackageName) viewModel = ViewModelProvider(this, factory).get(AgentAccessViewModel::class.java) val preferenceFragment = requirePreferenceFragment() preferenceFragment.lifecycleScope.launch { preferenceFragment.lifecycle.repeatOnLifecycle(State.STARTED) { viewModel.uiStateFlow.collect(::onUiStateChanged) } } } private fun onUiStateChanged(uiState: Stateful) { if (uiState is Stateful.Loading) { // do nothing return } val preferenceFragment = requirePreferenceFragment() val preferenceManager = preferenceFragment.preferenceManager var preferenceScreen = preferenceFragment.preferenceScreen val context = preferenceManager.context val oldPreferences = mutableMapOf() var oldSystemTargetPreferenceCategory: PreferenceCategory? = null val oldSystemTargetPreferences = mutableMapOf() var oldAppTargetPreferenceCategory: PreferenceCategory? = null val oldAppTargetPreferences = mutableMapOf() if (preferenceScreen == null) { preferenceScreen = preferenceManager.createPreferenceScreen(context) preferenceFragment.preferenceScreen = preferenceScreen } else { oldSystemTargetPreferenceCategory = preferenceScreen.findPreference(PREFERENCE_KEY_SYSTEM_CATEGORY) clearPreferenceCategory(oldSystemTargetPreferenceCategory, oldSystemTargetPreferences) oldAppTargetPreferenceCategory = preferenceScreen.findPreference(PREFERENCE_KEY_APP_CATEGORY) clearPreferenceCategory(oldAppTargetPreferenceCategory, oldAppTargetPreferences) clearPreferences(preferenceScreen, oldPreferences) } if (uiState is Stateful.Failure) { Log.e(LOG_TAG, "Failed to load target list", uiState.throwable) val agentLabel = uiState.value?.agent?.label ?: agentPackageName val agentIcon = uiState.value?.agent?.icon addHeaderPreference(preferenceScreen, agentLabel, agentIcon, oldPreferences) addEmptyStatePreference(preferenceScreen, agentLabel, oldPreferences) } else if (uiState is Stateful.Success) { val agentLabel = uiState.value.agent.label val agentIcon = uiState.value.agent.icon val targets = uiState.value.targets addHeaderPreference(preferenceScreen, agentLabel, agentIcon, oldPreferences) addSystemTargetPreferenceCategory( oldSystemTargetPreferenceCategory, preferenceScreen, uiState.value.deviceSettings, oldSystemTargetPreferences, context, ) addTargetPreferenceCategory( oldAppTargetPreferenceCategory, preferenceScreen, agentLabel, targets, oldAppTargetPreferences, context, ) } preferenceFragment.onPreferenceScreenChanged() } private fun clearPreferenceCategory( preferenceCategory: PreferenceCategory?, oldPreferences: MutableMap, ) { if (preferenceCategory == null) { return } clearPreferences(preferenceCategory, oldPreferences) preferenceCategory.parent?.removePreference(preferenceCategory) preferenceCategory.order = Preference.DEFAULT_ORDER } private fun clearPreferences( preferenceGroup: PreferenceGroup, oldPreferences: MutableMap, ) { for (i in preferenceGroup.preferenceCount - 1 downTo 0) { val preference = preferenceGroup.getPreference(i) preferenceGroup.removePreference(preference) preference.order = Preference.DEFAULT_ORDER oldPreferences[preference.key] = preference } } private fun addHeaderPreference( preferenceGroup: PreferenceGroup, agentLabel: String, agentIcon: Drawable?, oldPreferences: Map, ) { val preference = oldPreferences[PREFERENCE_KEY_INTRO] ?: requirePreferenceFragment().createHeaderPreference().apply { key = PREFERENCE_KEY_INTRO setSummary(R.string.app_function_agent_access_summary) } // Old preference might need to be updated preference.apply { icon = agentIcon title = agentLabel } preferenceGroup.addPreference(preference) } private fun addSystemTargetPreferenceCategory( oldPreferenceCategory: PreferenceCategory?, preferenceScreen: PreferenceScreen, deviceSettings: DeviceSettingsItem?, oldPreferences: Map, context: Context, ) { if (deviceSettings == null) { return } val preferenceCategory = oldPreferenceCategory ?: PreferenceCategory(context).apply { key = PREFERENCE_KEY_SYSTEM_CATEGORY setTitle(R.string.app_function_agent_access_system_targets_category_title) } preferenceScreen.addPreference(preferenceCategory) addDeviceSettingsTargetPreference(preferenceCategory, deviceSettings, oldPreferences) } private fun addTargetPreferenceCategory( oldPreferenceCategory: PreferenceCategory?, preferenceScreen: PreferenceScreen, agentLabel: String, targets: List, oldPreferences: Map, context: Context, ) { val preferenceCategory = oldPreferenceCategory ?: PreferenceCategory(context).apply { key = PREFERENCE_KEY_APP_CATEGORY setTitle(R.string.app_function_agent_access_app_targets_category_title) } preferenceScreen.addPreference(preferenceCategory) if (targets.isEmpty()) { addEmptyStatePreference(preferenceCategory, agentLabel, oldPreferences) } else { addTargetPreferences(preferenceCategory, targets, oldPreferences) } } private fun addEmptyStatePreference( preferenceGroup: PreferenceGroup, agentLabel: String, oldPreferences: Map, ) { val preference = oldPreferences[PREFERENCE_KEY_ZERO_STATE] ?: requirePreferenceFragment().createEmptyStatePreference().apply { key = PREFERENCE_KEY_ZERO_STATE title = getString( R.string.app_function_agent_access_app_targets_empty_title, agentLabel, ) } preferenceGroup.addPreference(preference) } private fun addDeviceSettingsTargetPreference( preferenceGroup: PreferenceGroup, target: DeviceSettingsItem, oldPreferences: Map, ) { val preference = oldPreferences[PREFERENCE_KEY_DEVICE_SETTINGS] as TwoStatePreference? ?: requirePreferenceFragment().createPreference().apply { key = PREFERENCE_KEY_DEVICE_SETTINGS onPreferenceClickListener = this@AgentAccessChildFragment } // Ensure current ux state reflects the data state preference.apply { setTitle(R.string.app_function_device_settings_target_title) if (target.icon != null) { icon = target.icon } else { setIcon(R.drawable.ic_appfunction_target_device_settings) } isChecked = target.accessGranted } preferenceGroup.addPreference(preference) } private fun addTargetPreferences( preferenceGroup: PreferenceGroup, targets: List, oldPreferences: Map, ) { for (target in targets) { val targetPackageInfo = target.packageInfo val targetPackageName = targetPackageInfo.packageName val preference = oldPreferences[targetPackageName] as TwoStatePreference? ?: requirePreferenceFragment().createPreference().apply { key = targetPackageName onPreferenceClickListener = this@AgentAccessChildFragment } // Ensure current ux state reflects the data state preference.apply { title = targetPackageInfo.label icon = targetPackageInfo.icon isChecked = target.accessGranted } preferenceGroup.addPreference(preference) } } override fun onPreferenceClick(preference: Preference): Boolean { preference as TwoStatePreference if (preference.key == PREFERENCE_KEY_DEVICE_SETTINGS) { viewModel.updateDeviceSettingsAccessState(preference.isChecked) } else { viewModel.updateAccessState(preference.key, preference.isChecked) } return true } private fun requirePreferenceFragment(): PF { @Suppress("UNCHECKED_CAST") return requireParentFragment() as PF } /** Interface that the parent fragment must implement. */ interface Parent { /** Creates a new header preference for the screen */ fun createHeaderPreference(): Preference /** Creates a new empty state preference for the screen */ fun createEmptyStatePreference(): Preference /** Creates a new two state preference for a target app. */ fun createPreference(): TwoStatePreference /** * Callback when changes have been made to the {@link PreferenceScreen} of the parent {@link * PreferenceFragmentCompat}. */ fun onPreferenceScreenChanged() } companion object { private val LOG_TAG = AgentAccessChildFragment::class.java.simpleName private val PREFERENCE_KEY_INTRO = AgentAccessChildFragment::class.java.name + ".preference.INTRO" private val PREFERENCE_KEY_SYSTEM_CATEGORY = AgentAccessChildFragment::class.java.name + ".preference.SYSTEM_CATEGORY" private val PREFERENCE_KEY_APP_CATEGORY = AgentAccessChildFragment::class.java.name + ".preference.APP_CATEGORY" private val PREFERENCE_KEY_ZERO_STATE = AgentAccessChildFragment::class.java.name + ".preference.APP_ZERO_STATE" private val PREFERENCE_KEY_DEVICE_SETTINGS = AgentAccessChildFragment::class.java.name + ".preference.DEVICE_SETTINGS" /** * Create a new instance of AgentAccessChildFragment * * @param agentPackageName agent package to modify access for * @return a new instance of AgentAccessChildFragment */ fun newInstance(agentPackageName: String): AgentAccessChildFragment<*> { val arguments = Bundle().apply { putString(Intent.EXTRA_PACKAGE_NAME, agentPackageName) } return AgentAccessChildFragment().apply { setArguments(arguments) } } } }