chromium/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.browser_ui.site_settings;

import static org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge.SITE_WILDCARD;

import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.TextView;

import androidx.annotation.ColorRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;

import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.components.browser_ui.settings.ChromeImageViewPreference;
import org.chromium.components.browser_ui.settings.ChromeSwitchPreference;
import org.chromium.components.browser_ui.settings.CustomDividerFragment;
import org.chromium.components.browser_ui.settings.ManagedPreferencesUtils;
import org.chromium.components.browser_ui.settings.SettingsPage;
import org.chromium.components.browser_ui.settings.SettingsUtils;
import org.chromium.components.browser_ui.settings.TextMessagePreference;
import org.chromium.components.browsing_data.DeleteBrowsingDataAction;
import org.chromium.components.content_settings.ContentSettingValues;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.components.content_settings.SessionModel;
import org.chromium.components.embedder_support.util.Origin;
import org.chromium.content_public.browser.BrowserContextHandle;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** Shows the permissions and other settings for a particular website. */
public class SingleWebsiteSettings extends BaseSiteSettingsFragment
        implements SettingsPage,
                Preference.OnPreferenceChangeListener,
                Preference.OnPreferenceClickListener,
                CustomDividerFragment {
    /** Interface for a class that wants to receive updates from SingleWebsiteSettings. */
    public interface Observer {
        /** Notifies the observer that the website was reset. */
        void onPermissionsReset();

        /** Notifies the observer that a permission was changed. */
        void onPermissionChanged();
    }

    // SingleWebsiteSettings expects either EXTRA_SITE (a Website) or
    // EXTRA_SITE_ADDRESS (a WebsiteAddress) to be present (but not both). If
    // EXTRA_SITE is present, the fragment will display the permissions in that
    // Website object. If EXTRA_SITE_ADDRESS is present, the fragment will find all
    // permissions for that website address and display those.
    public static final String EXTRA_SITE = "org.chromium.chrome.preferences.site";
    public static final String EXTRA_SITE_ADDRESS = "org.chromium.chrome.preferences.site_address";

    // A boolean to configure whether the sound setting should be shown. Defaults to true.
    public static final String EXTRA_SHOW_SOUND = "org.chromium.chrome.preferences.show_sound";

    // A boolean that indicates whether these settings were opened from GroupedWebsiteSettings.
    public static final String EXTRA_FROM_GROUPED = "org.chromium.chrome.preferences.from_grouped";

    // Used to store mPreviousNotificationPermission when the activity is paused.
    private static final String PREVIOUS_NOTIFICATION_PERMISSION_KEY =
            "previous_notification_permission";

    // Preference keys, see single_website_preferences.xml
    // Headings:
    public static final String PREF_PAGE_DESCRIPTION = "page_description";
    public static final String PREF_SITE_HEADING = "site_heading";
    public static final String PREF_SITE_TITLE = "site_title";
    public static final String PREF_USAGE = "site_usage";
    public static final String PREF_RELATED_SITES_HEADER = "related_sites_header";
    public static final String PREF_RELATED_SITES = "related_sites";
    public static final String PREF_PERMISSIONS_HEADER = "site_permissions";
    public static final String PREF_OS_PERMISSIONS_WARNING = "os_permissions_warning";
    public static final String PREF_OS_PERMISSIONS_WARNING_EXTRA = "os_permissions_warning_extra";
    public static final String PREF_OS_PERMISSIONS_WARNING_DIVIDER =
            "os_permissions_warning_divider";
    public static final String PREF_INTRUSIVE_ADS_INFO = "intrusive_ads_info";
    public static final String PREF_INTRUSIVE_ADS_INFO_DIVIDER = "intrusive_ads_info_divider";
    // Actions at the top (if adding new, see hasUsagePreferences below):
    public static final String PREF_CLEAR_DATA = "clear_data";
    // Buttons:
    public static final String PREF_RESET_SITE = "reset_site_button";

    public static final int REQUEST_CODE_NOTIFICATION_CHANNEL_SETTINGS = 1;

    private static boolean arrayContains(int[] array, int element) {
        for (int e : array) {
            if (e == element) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param type ContentSettingsType
     * @return The preference key of this type
     */
    @VisibleForTesting
    public static @Nullable String getPreferenceKey(@ContentSettingsType.EnumType int type) {
        switch (type) {
            case ContentSettingsType.ADS:
                return "ads_permission_list";
            case ContentSettingsType.AUTO_DARK_WEB_CONTENT:
                return "auto_dark_web_content_permission_list";
            case ContentSettingsType.AUTOMATIC_DOWNLOADS:
                return "automatic_downloads_permission_list";
            case ContentSettingsType.BACKGROUND_SYNC:
                return "background_sync_permission_list";
            case ContentSettingsType.BLUETOOTH_SCANNING:
                return "bluetooth_scanning_permission_list";
            case ContentSettingsType.COOKIES:
                return "cookies_permission_list";
            case ContentSettingsType.FEDERATED_IDENTITY_API:
                return "federated_identity_api_list";
            case ContentSettingsType.IDLE_DETECTION:
                return "idle_detection_permission_list";
            case ContentSettingsType.JAVASCRIPT:
                return "javascript_permission_list";
            case ContentSettingsType.POPUPS:
                return "popup_permission_list";
            case ContentSettingsType.SOUND:
                return "sound_permission_list";
            case ContentSettingsType.AR:
                return "ar_permission_list";
            case ContentSettingsType.MEDIASTREAM_CAMERA:
                return "camera_permission_list";
            case ContentSettingsType.GEOLOCATION:
                return "location_access_list";
            case ContentSettingsType.HAND_TRACKING:
                return "hand_tracking_permission_list";
            case ContentSettingsType.MEDIASTREAM_MIC:
                return "microphone_permission_list";
            case ContentSettingsType.MIDI_SYSEX:
                return "midi_sysex_permission_list";
            case ContentSettingsType.NFC:
                return "nfc_permission_list";
            case ContentSettingsType.NOTIFICATIONS:
                return "push_notifications_list";
            case ContentSettingsType.PROTECTED_MEDIA_IDENTIFIER:
                return "protected_media_identifier_permission_list";
            case ContentSettingsType.REQUEST_DESKTOP_SITE:
                return "request_desktop_site_permission_list";
            case ContentSettingsType.SENSORS:
                return "sensors_permission_list";
            case ContentSettingsType.VR:
                return "vr_permission_list";
            case ContentSettingsType.CLIPBOARD_READ_WRITE:
                return "clipboard_permission_list";
            default:
                return null;
        }
    }

    // A list of preferences keys that will be hidden on this page if this boolean below is true
    private boolean mHideNonPermissionPreferences;
    private static final String[] NON_PERMISSION_PREFERENCES = {
        PREF_SITE_HEADING,
        PREF_SITE_TITLE,
        PREF_USAGE,
        PREF_RELATED_SITES_HEADER,
        PREF_RELATED_SITES,
        PREF_PERMISSIONS_HEADER,
        PREF_CLEAR_DATA,
    };

    /** The permission type to be highlighted on this page, if any. */
    @ContentSettingsType.EnumType private int mHighlightedPermission = ContentSettingsType.DEFAULT;

    /** The highlight color. */
    @ColorRes private int mHighlightColor;

    // The callback to be run after this site is reset.
    private Observer mWebsiteSettingsObserver;

    // The website this page is displaying details about.
    private Website mSite;

    // Whether these settings were opened from GroupedWebsitesSettings.
    private boolean mFromGrouped;

    private final List<ChromeSwitchPreference> mEmbeddedPermissionPreferences = new ArrayList<>();

    private final List<ChromeImageViewPreference> mChooserPermissionPreferences = new ArrayList<>();

    // Records previous notification permission on Android O+ to allow detection of permission
    // revocation within the Android system permission activity.
    private @ContentSettingValues @Nullable Integer mPreviousNotificationPermission;

    // Map from preference key to ContentSettingsType.
    private Map<String, Integer> mPreferenceMap;

    private Dialog mConfirmationDialog;

    // Maximum value used for the order of the permissions
    private int mMaxPermissionOrder;

    private final ObservableSupplierImpl<String> mPageTitle = new ObservableSupplierImpl<>();

    private class SingleWebsitePermissionsPopulator
            implements WebsitePermissionsFetcher.WebsitePermissionsCallback {
        private final WebsiteAddress mSiteAddress;

        public SingleWebsitePermissionsPopulator(WebsiteAddress siteAddress) {
            mSiteAddress = siteAddress;
        }

        @Override
        public void onWebsitePermissionsAvailable(Collection<Website> sites) {
            // This method may be called after the activity has been destroyed.
            // In that case, bail out.
            if (getActivity() == null) return;

            // TODO(mvanouwerkerk): Avoid modifying the outer class from this inner class.
            mSite = mergePermissionAndStorageInfoForTopLevelOrigin(mSiteAddress, sites);

            displaySitePermissions();
        }
    }

    private final Runnable mDataClearedCallback =
            () -> {
                Activity activity = getActivity();
                if (activity == null || activity.isFinishing()) {
                    return;
                }
                removePreferenceSafely(PREF_CLEAR_DATA);
                if (!hasUsagePreferences()) {
                    removePreferenceSafely(PREF_USAGE);
                }
                removeUserChosenObjectPreferences();
                popBackIfNoSettings();
            };

    /**
     * Creates a Bundle with the correct arguments for opening this fragment for
     * the website with the given url.
     *
     * @param url The URL to open the fragment with. This is a complete url including scheme,
     *            domain, port,  path, etc.
     * @return The bundle to attach to the preferences intent.
     */
    public static Bundle createFragmentArgsForSite(String url) {
        Bundle fragmentArgs = new Bundle();
        String origin = Origin.createOrThrow(url).toString();
        fragmentArgs.putSerializable(EXTRA_SITE_ADDRESS, WebsiteAddress.create(origin));
        return fragmentArgs;
    }

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        // Handled in displaySitePermissions. Moving the addPreferencesFromResource call up to here
        // causes animation jank (crbug.com/985734).
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        if (mPreviousNotificationPermission != null) {
            outState.putInt(PREVIOUS_NOTIFICATION_PERMISSION_KEY, mPreviousNotificationPermission);
        }
        super.onSaveInstanceState(outState);
    }

    @Override
    public void onViewStateRestored(Bundle savedInstanceState) {
        super.onViewStateRestored(savedInstanceState);

        if (savedInstanceState == null) return;

        if (savedInstanceState.containsKey(PREVIOUS_NOTIFICATION_PERMISSION_KEY)) {
            mPreviousNotificationPermission =
                    savedInstanceState.getInt(PREVIOUS_NOTIFICATION_PERMISSION_KEY);
        }
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        mPageTitle.set(getContext().getString(R.string.prefs_site_settings));
        init();
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public ObservableSupplier<String> getPageTitle() {
        return mPageTitle;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        if (mConfirmationDialog != null) {
            mConfirmationDialog.dismiss();
        }
    }

    private void init() {
        // Remove this Preference if it gets restored without a valid SiteSettingsDelegate. This
        // can happen e.g. when it is included in PageInfo.
        if (!hasSiteSettingsDelegate()) {
            getParentFragmentManager().beginTransaction().remove(this).commit();
            return;
        }

        Object extraSite = getArguments().getSerializable(EXTRA_SITE);
        Object extraSiteAddress = getArguments().getSerializable(EXTRA_SITE_ADDRESS);

        if (extraSite != null && extraSiteAddress == null) {
            mSite = (Website) extraSite;
            displaySitePermissions();
        } else if (extraSiteAddress != null && extraSite == null) {
            WebsitePermissionsFetcher fetcher =
                    new WebsitePermissionsFetcher(getSiteSettingsDelegate());
            fetcher.fetchAllPreferences(
                    new SingleWebsitePermissionsPopulator((WebsiteAddress) extraSiteAddress));
        } else {
            assert false : "Exactly one of EXTRA_SITE or EXTRA_SITE_ADDRESS must be provided.";
        }

        mFromGrouped = getArguments().getBoolean(EXTRA_FROM_GROUPED, false);

        // Disable animations of preference changes.
        getListView().setItemAnimator(null);
    }

    @Override
    public boolean hasDivider() {
        return false;
    }

    @Override
    public void onDisplayPreferenceDialog(Preference preference) {
        if (preference instanceof ClearWebsiteStorage) {
            // If the activity is getting destroyed or saved, it is not allowed to modify fragments.
            if (getFragmentManager().isStateSaved()) {
                return;
            }
            Callback<Boolean> onDialogClosed =
                    (Boolean confirmed) -> {
                        if (confirmed) {
                            RecordHistogram.recordEnumeratedHistogram(
                                    "Privacy.DeleteBrowsingData.Action",
                                    DeleteBrowsingDataAction.SITES_SETTINGS_PAGE,
                                    DeleteBrowsingDataAction.MAX_VALUE);

                            SiteDataCleaner.clearData(
                                    getSiteSettingsDelegate(), mSite, mDataClearedCallback);
                        }
                    };
            ClearWebsiteStorageDialog dialogFragment =
                    ClearWebsiteStorageDialog.newInstance(
                            preference, onDialogClosed, /* isGroup= */ false);
            dialogFragment.setTargetFragment(this, 0);
            dialogFragment.show(getFragmentManager(), ClearWebsiteStorageDialog.TAG);
        } else {
            super.onDisplayPreferenceDialog(preference);
        }
    }

    public void setHideNonPermissionPreferences(boolean hide) {
        mHideNonPermissionPreferences = hide;
    }

    public void setWebsiteSettingsObserver(Observer observer) {
        mWebsiteSettingsObserver = observer;
    }

    /**
     * Sets the permission row that should be highlighted on the page, with its corresponding color.
     *
     * @param permission The ContentSettingsType for the permission to be highlighted.
     * @param colorResId The color resource id for the background color of the permission row.
     */
    public void setHighlightedPermission(
            @ContentSettingsType.EnumType int permission, @ColorRes int colorResId) {
        mHighlightedPermission = permission;
        mHighlightColor = colorResId;
    }

    /**
     * Given an address and a list of sets of websites, returns a new site with the same origin
     * as |address| which has merged into it the permissions and storage info of the matching input
     * sites. If a permission is found more than once, the one found first is used and the latter
     * are ignored. This should not drop any relevant data as there should not be duplicates like
     * that in the first place.
     *
     * @param address The address to search for.
     * @param websites The websites to search in.
     * @return The merged website.
     */
    public static Website mergePermissionAndStorageInfoForTopLevelOrigin(
            WebsiteAddress address, Collection<Website> websites) {
        String origin = address.getOrigin();
        String host = Uri.parse(origin).getHost();
        String domainAndRegistry = address.getDomainAndRegistry();
        Website merged = new Website(address, null);
        // This loop looks expensive, but the amount of data is likely to be relatively small
        // because most sites have very few permissions.
        for (Website other : websites) {
            if (merged.getContentSettingException(ContentSettingsType.ADS) == null
                    && other.getContentSettingException(ContentSettingsType.ADS) != null
                    && other.compareByAddressTo(merged) == 0) {
                merged.setContentSettingException(
                        ContentSettingsType.ADS,
                        other.getContentSettingException(ContentSettingsType.ADS));
            }
            for (PermissionInfo info : other.getPermissionInfos()) {
                if (merged.getPermissionInfo(info.getContentSettingsType()) == null
                        && permissionInfoIsForTopLevelOrigin(info, origin)) {
                    merged.setPermissionInfo(info);
                }
            }
            for (var exceptionList : other.getEmbeddedPermissions().values()) {
                for (var exception : exceptionList) {
                    boolean matchesOrigin =
                            other.getEmbedder() != null
                                    && org.chromium.components.browser_ui.site_settings
                                            .WebsitePreferenceBridgeJni.get()
                                            .urlMatchesContentSettingsPattern(
                                                    origin, exception.getSecondaryPattern());
                    if (matchesOrigin) {
                        merged.addEmbeddedPermission(exception);
                    }
                }
            }

            if (merged.getLocalStorageInfo() == null
                    && other.getLocalStorageInfo() != null
                    && origin.equals(other.getLocalStorageInfo().getOrigin())) {
                merged.setLocalStorageInfo(other.getLocalStorageInfo());
            }
            for (StorageInfo storageInfo : other.getStorageInfo()) {
                if (host.equals(storageInfo.getHost())) {
                    merged.addStorageInfo(storageInfo);
                }
            }
            for (SharedDictionaryInfo sharedDictionaryInfo : other.getSharedDictionaryInfo()) {
                if (origin.equals(sharedDictionaryInfo.getOrigin())) {
                    merged.addSharedDictionaryInfo(sharedDictionaryInfo);
                }
            }
            if (merged.getRWSCookieInfo() == null
                    && other.getRWSCookieInfo() != null
                    && domainAndRegistry.equals(other.getAddress().getDomainAndRegistry())) {
                merged.setRWSCookieInfo(other.getRWSCookieInfo());
            }
            for (ChosenObjectInfo objectInfo : other.getChosenObjectInfo()) {
                if (origin.equals(objectInfo.getOrigin())) {
                    merged.addChosenObjectInfo(objectInfo);
                }
            }
            if (host.equals(other.getAddress().getHost())) {
                for (ContentSettingException exception : other.getContentSettingExceptions()) {
                    int type = exception.getContentSettingType();
                    if (type == ContentSettingsType.ADS) {
                        continue;
                    }
                    if (merged.getContentSettingException(type) == null) {
                        merged.setContentSettingException(type, exception);
                    }
                }
            }

            merged.setDomainImportant(merged.isDomainImportant() || other.isDomainImportant());

            // TODO(crbug.com/40539464): Deal with this TODO colony.
            // TODO(mvanouwerkerk): Make the various info types share a common interface that
            // supports reading the origin or host.
            // TODO(lshang): Merge in CookieException? It will use patterns.
        }
        return merged;
    }

    private static boolean permissionInfoIsForTopLevelOrigin(PermissionInfo info, String origin) {
        // TODO(mvanouwerkerk): Find a more generic place for this method.
        return origin.equals(info.getOrigin())
                && (origin.equals(info.getEmbedderSafe())
                        || SITE_WILDCARD.equals(info.getEmbedderSafe()));
    }

    private Drawable getContentSettingsIcon(
            @ContentSettingsType.EnumType int contentSettingsType,
            @ContentSettingValues @Nullable Integer value) {
        return ContentSettingsResources.getContentSettingsIcon(
                getContext(), contentSettingsType, value);
    }

    /**
     * Updates the permissions displayed in the UI by fetching them from mSite. Must only be called
     * once mSite is set.
     */
    private void displaySitePermissions() {
        if (getPreferenceScreen() != null) {
            getPreferenceScreen().removeAll();
        }
        SettingsUtils.addPreferencesFromResource(this, R.xml.single_website_preferences);

        findPreference(PREF_SITE_TITLE).setTitle(mSite.getTitle());
        setupContentSettingsPreferences();
        setUpEmbeddedContentSettingPreferences();
        setUpChosenObjectPreferences();
        setupResetSitePreference();
        setUpClearDataPreference();
        setUpOsWarningPreferences();
        setupRelatedSitesPreferences();

        setUpAdsInformationalBanner();

        // Remove categories if no sub-items.
        if (!hasUsagePreferences()) {
            removePreferenceSafely(PREF_USAGE);
        }
        if (!hasPermissionsPreferences()) {
            removePreferenceSafely(PREF_PERMISSIONS_HEADER);
        }

        // Remove certain preferences explicitly
        if (mHideNonPermissionPreferences) {
            for (String key : NON_PERMISSION_PREFERENCES) {
                removePreferenceSafely(key);
            }
        } else {
            removePreferenceSafely(PREF_PAGE_DESCRIPTION);
        }
    }

    private void setupContentSettingsPreferences() {
        mMaxPermissionOrder = findPreference(PREF_PERMISSIONS_HEADER).getOrder();
        for (@ContentSettingsType.EnumType int type : SiteSettingsUtil.SETTINGS_ORDER) {
            Preference preference = new ChromeSwitchPreference(getStyledContext());
            preference.setKey(getPreferenceKey(type));

            if (type == ContentSettingsType.ADS) {
                setUpAdsPreference(preference);
            } else if (type == ContentSettingsType.SOUND) {
                setUpSoundPreference(preference);
            } else if (type == ContentSettingsType.JAVASCRIPT) {
                setUpJavascriptPreference(preference);
            } else if (type == ContentSettingsType.GEOLOCATION) {
                setUpLocationPreference(preference);
            } else if (type == ContentSettingsType.NOTIFICATIONS) {
                setUpNotificationsPreference(preference, mSite.isEmbargoed(type));
            } else {
                setupContentSettingsPreference(
                        preference,
                        mSite.getContentSetting(
                                getSiteSettingsDelegate().getBrowserContextHandle(), type),
                        mSite.isEmbargoed(type),
                        isOneTime(type));
            }
        }
    }

    private void setUpClearDataPreference() {
        ClearWebsiteStorage preference = findPreference(PREF_CLEAR_DATA);
        long usage = mSite.getTotalUsage();
        int cookies = mSite.getNumberOfCookies();
        // Only take cookies into account when the new UI is enabled.
        if (usage > 0 || cookies > 0) {
            boolean appFound =
                    getSiteSettingsDelegate()
                            .getOriginsWithInstalledApp()
                            .contains(mSite.getAddress().getOrigin());
            Context context = preference.getContext();
            preference.setTitle(SiteSettingsUtil.generateStorageUsageText(context, usage, cookies));
            preference.setDataForDisplay(mSite.getTitle(), appFound, /* isGroup= */ false);
            if (mSite.isCookieDeletionDisabled(
                    getSiteSettingsDelegate().getBrowserContextHandle())) {
                preference.setEnabled(false);
            }
        } else {
            getPreferenceScreen().removePreference(preference);
        }
    }

    private void setupResetSitePreference() {
        Preference preference = findPreference(PREF_RESET_SITE);
        preference.setTitle(
                mHideNonPermissionPreferences
                        ? R.string.page_info_permissions_reset
                        : R.string.website_reset_full);
        preference.setOrder(mMaxPermissionOrder + 1);
        preference.setOnPreferenceClickListener(this);
        if (mSite.isCookieDeletionDisabled(getSiteSettingsDelegate().getBrowserContextHandle())) {
            preference.setEnabled(false);
        }
    }

    private Intent getSettingsIntent(String packageName, @ContentSettingsType.EnumType int type) {
        Intent intent = new Intent();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
                && type == ContentSettingsType.NOTIFICATIONS) {
            intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
            intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
        } else {
            intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
            intent.setData(Uri.parse("package:" + packageName));
        }
        return intent;
    }

    /**
     * Replaces a Preference with a read-only copy. The new Preference retains
     * its key and the order within the preference screen, but gets a new
     * summary and (intentionally) loses its click handler.
     * @return A read-only copy of the preference passed in as |oldPreference|.
     */
    private ChromeImageViewPreference createReadOnlyCopyOf(
            Preference oldPreference,
            String newSummary,
            @ContentSettingValues @Nullable Integer value) {
        ChromeImageViewPreference newPreference =
                new ChromeImageViewPreference(oldPreference.getContext());
        newPreference.setKey(oldPreference.getKey());
        setUpPreferenceCommon(newPreference, value);
        newPreference.setSummary(newSummary);
        @ContentSettingsType.EnumType
        int contentType = getContentSettingsTypeFromPreferenceKey(newPreference.getKey());
        if (contentType == mHighlightedPermission) {
            newPreference.setBackgroundColor(mHighlightColor);
        }

        return newPreference;
    }

    /**
     * A permission can be managed by an app. For example, with a Chrome SiteSettingsDelegate,
     * Notifications could be controlled by PWA, however for a Weblayer variant, Location could be
     * controlled by the DSE.
     */
    private boolean setupAppDelegatePreference(
            Preference preference,
            @StringRes int contentDescriptionRes,
            @ContentSettingsType.EnumType int type,
            @ContentSettingValues @Nullable Integer value) {
        Origin origin = Origin.create(mSite.getAddress().getOrigin());
        if (origin == null) {
            return false;
        }

        String managedByAppName =
                getSiteSettingsDelegate().getDelegateAppNameForOrigin(origin, type);
        if (managedByAppName == null) {
            return false;
        }

        final Intent settingsIntent =
                getSettingsIntent(
                        getSiteSettingsDelegate().getDelegatePackageNameForOrigin(origin, type),
                        type);
        String summaryText = getString(R.string.website_setting_managed_by_app, managedByAppName);
        ChromeImageViewPreference newPreference =
                createReadOnlyCopyOf(preference, summaryText, value);

        newPreference.setImageView(R.drawable.permission_popups, contentDescriptionRes, null);
        // By disabling the ImageView, clicks will go through to the preference.
        newPreference.setImageViewEnabled(false);

        newPreference.setOnPreferenceClickListener(
                unused -> {
                    startActivity(settingsIntent);
                    return true;
                });
        return true;
    }

    private void setUpNotificationsPreference(Preference preference, boolean isEmbargoed) {
        @ContentSettingsType.EnumType int notificationType = ContentSettingsType.NOTIFICATIONS;
        final @ContentSettingValues @Nullable Integer value =
                mSite.getContentSetting(
                        getSiteSettingsDelegate().getBrowserContextHandle(), notificationType);
        if (setupAppDelegatePreference(
                preference, R.string.website_notification_settings, notificationType, value)) {
            return;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (value == null
                    || (value != ContentSettingValues.ALLOW
                            && value != ContentSettingValues.BLOCK)) {
                // TODO(crbug.com/40526685): Figure out if this is the correct thing to do, for
                // values
                // that are non-null, but not ALLOW or BLOCK either. (In
                // setupContentSettingsPreference we treat non-ALLOW settings as BLOCK, but here we
                // are simply not adding it.)
                return;
            }
            String overrideSummary;
            overrideSummary =
                    isEmbargoed
                            ? getString(R.string.automatically_blocked)
                            : getString(
                                    ContentSettingsResources.getCategorySummary(
                                            value, isOneTime(notificationType)));

            // On Android O this preference is read-only, so we replace the existing pref with a
            // regular Preference that takes users to OS settings on click.
            ChromeImageViewPreference newPreference =
                    createReadOnlyCopyOf(preference, overrideSummary, value);
            newPreference.setImageView(
                    R.drawable.permission_popups,
                    0,
                    unused -> launchOsChannelSettingsFromPreference(preference));
            newPreference.setImageColor(R.color.default_icon_color_secondary_tint_list);
            newPreference.setDefaultValue(value);

            newPreference.setOnPreferenceClickListener(
                    unused -> {
                        launchOsChannelSettingsFromPreference(preference);
                        return true;
                    });
        } else {
            setupContentSettingsPreference(
                    preference, value, isEmbargoed, isOneTime(notificationType));
        }
    }

    // This is implemented as a public utility function to better facilitate testing.
    @VisibleForTesting
    public void launchOsChannelSettingsFromPreference(Preference preference) {
        // There is no notification channel if the origin is merely embargoed. Create it
        // just-in-time if the user tries to change to setting.
        if (mSite.isEmbargoed(ContentSettingsType.NOTIFICATIONS)) {
            mSite.setContentSetting(
                    getSiteSettingsDelegate().getBrowserContextHandle(),
                    ContentSettingsType.NOTIFICATIONS,
                    ContentSettingValues.BLOCK);
        }

        // There is no guarantee that a channel has been initialized yet for sites
        // that were granted permission before the channel-initialization-on-grant
        // code was in place. However, getChannelIdForOrigin will fall back to the
        // generic Sites channel if no specific channel has been created for the given
        // origin, so it is safe to open the channel settings for whatever channel ID
        // it returns.
        String channelId =
                getSiteSettingsDelegate().getChannelIdForOrigin(mSite.getAddress().getOrigin());
        launchOsChannelSettings(preference.getContext(), channelId);
    }

    private void launchOsChannelSettings(Context context, String channelId) {
        // Store current value of permission to allow comparison against new value at return.
        mPreviousNotificationPermission =
                mSite.getContentSetting(
                        getSiteSettingsDelegate().getBrowserContextHandle(),
                        ContentSettingsType.NOTIFICATIONS);

        Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
        intent.putExtra(Settings.EXTRA_CHANNEL_ID, channelId);
        intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
        startActivityForResult(intent, REQUEST_CODE_NOTIFICATION_CHANNEL_SETTINGS);
    }

    /**
     * If we are returning to Site Settings from another activity, the preferences displayed may be
     * out of date. Here we refresh any we suspect may have changed.
     */
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        // The preference screen and mSite may be null if this activity was killed in the
        // background, and the tasks scheduled from onActivityCreated haven't completed yet. Those
        // tasks will take care of reinitializing everything afresh so there is no work to do here.
        if (getPreferenceScreen() == null || mSite == null) {
            return;
        }
        if (requestCode == REQUEST_CODE_NOTIFICATION_CHANNEL_SETTINGS) {
            @ContentSettingValues
            int newPermission =
                    mSite.getContentSetting(
                            getSiteSettingsDelegate().getBrowserContextHandle(),
                            ContentSettingsType.NOTIFICATIONS);
            // User has navigated back from system channel settings on O+. Ensure notification
            // preference is up to date, since they might have toggled it from channel settings.
            Preference notificationsPreference =
                    findPreference(getPreferenceKey(ContentSettingsType.NOTIFICATIONS));
            if (notificationsPreference != null) {
                onPreferenceChange(notificationsPreference, (Object) newPermission);
            }

            // To ensure UMA receives notification revocations, we detect if the setting has changed
            // after returning to Chrome.  This is lossy, as it will miss when users revoke a
            // permission, but do not return immediately to Chrome (e.g. they close the permissions
            // activity, instead of hitting the back button), but prevents us from having to check
            // for changes each time Chrome becomes active.
            if (mPreviousNotificationPermission == ContentSettingValues.ALLOW
                    && newPermission != ContentSettingValues.ALLOW) {
                org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridgeJni.get()
                        .reportNotificationRevokedForOrigin(
                                getSiteSettingsDelegate().getBrowserContextHandle(),
                                mSite.getAddress().getOrigin(),
                                newPermission);
                mPreviousNotificationPermission = null;
            }
        }
    }

    /**
     * Creates a ChromeImageViewPreference for each object permission with a
     * ManagedPreferenceDelegate that configures the Preference's widget to display a managed icon
     * and show a toast if a managed permission is clicked. The preferences are added to the
     * preference screen using |maxPermissionOrder| to order the preferences in the list.
     */
    private void setUpChosenObjectPreferences() {
        PreferenceScreen preferenceScreen = getPreferenceScreen();

        for (ChosenObjectInfo info : mSite.getChosenObjectInfo()) {
            ChromeImageViewPreference preference =
                    new ChromeImageViewPreference(getStyledContext());
            assert arrayContains(
                    SiteSettingsUtil.CHOOSER_PERMISSIONS, info.getContentSettingsType());
            mChooserPermissionPreferences.add(preference);
            preference.setIcon(getContentSettingsIcon(info.getContentSettingsType(), null));
            preference.setTitle(info.getName());
            preference.setImageView(
                    R.drawable.ic_delete_white_24dp,
                    R.string.website_settings_revoke_device_permission,
                    (View view) -> {
                        info.revoke(getSiteSettingsDelegate().getBrowserContextHandle());
                        preferenceScreen.removePreference(preference);
                        mChooserPermissionPreferences.remove(preference);

                        if (!hasPermissionsPreferences()) {
                            removePreferenceSafely(PREF_PERMISSIONS_HEADER);
                        }
                    });
            if (info.getContentSettingsType() == mHighlightedPermission) {
                preference.setBackgroundColor(mHighlightColor);
            }

            preference.setManagedPreferenceDelegate(
                    new ForwardingManagedPreferenceDelegate(
                            getSiteSettingsDelegate().getManagedPreferenceDelegate()) {
                        @Override
                        public boolean isPreferenceControlledByPolicy(Preference preference) {
                            return info.isManaged();
                        }
                    });

            preference.setOrder(++mMaxPermissionOrder);
            preferenceScreen.addPreference(preference);
        }
    }

    String getEmbeddedPermissionSummary(String embeddedHost, @ContentSettingValues int setting) {
        int id =
                setting == ContentSettingValues.ALLOW
                        ? R.string.website_settings_site_allowed
                        : R.string.website_settings_site_blocked;
        return getContext().getResources().getString(id, embeddedHost);
    }

    private void setUpEmbeddedContentSettingPreferences() {
        PreferenceScreen preferenceScreen = getPreferenceScreen();
        BrowserContextHandle handle = getSiteSettingsDelegate().getBrowserContextHandle();

        for (List<ContentSettingException> entries : mSite.getEmbeddedPermissions().values()) {
            for (ContentSettingException info : entries) {
                assert arrayContains(
                        SiteSettingsUtil.EMBEDDED_PERMISSIONS, info.getContentSettingType());
                var preference = new ChromeSwitchPreference(getStyledContext());
                mEmbeddedPermissionPreferences.add(preference);
                preference.setIcon(
                        getContentSettingsIcon(
                                info.getContentSettingType(), info.getContentSetting()));
                preference.setTitle(
                        ContentSettingsResources.getTitle(info.getContentSettingType()));
                var pattern = WebsiteAddress.create(info.getPrimaryPattern());
                preference.setSummary(
                        getEmbeddedPermissionSummary(pattern.getHost(), info.getContentSetting()));

                preference.setChecked(info.getContentSetting() == ContentSettingValues.ALLOW);
                preference.setOnPreferenceChangeListener(
                        (pref, newValue) -> {
                            @ContentSettingValues
                            int contentSetting =
                                    (boolean) newValue
                                            ? ContentSettingValues.ALLOW
                                            : ContentSettingValues.BLOCK;
                            info.setContentSetting(handle, contentSetting);
                            preference.setSummary(
                                    getEmbeddedPermissionSummary(
                                            pattern.getHost(), contentSetting));
                            preference.setIcon(
                                    getContentSettingsIcon(
                                            info.getContentSettingType(), contentSetting));

                            if (mWebsiteSettingsObserver != null) {
                                mWebsiteSettingsObserver.onPermissionChanged();
                            }
                            return true;
                        });
                if (info.getContentSettingType() == mHighlightedPermission) {
                    preference.setBackgroundColor(
                            AppCompatResources.getColorStateList(getContext(), mHighlightColor)
                                    .getDefaultColor());
                }

                preference.setOrder(++mMaxPermissionOrder);
                preferenceScreen.addPreference(preference);
            }
        }
    }

    private Context getStyledContext() {
        return getPreferenceManager().getContext();
    }

    private void setUpOsWarningPreferences() {
        PreferenceScreen preferenceScreen = getPreferenceScreen();
        SiteSettingsCategory categoryWithWarning = getWarningCategory();
        // Remove the 'permission is off in Android' message if not needed.
        if (categoryWithWarning == null) {
            removePreferenceSafely(PREF_OS_PERMISSIONS_WARNING);
            removePreferenceSafely(PREF_OS_PERMISSIONS_WARNING_EXTRA);
            removePreferenceSafely(PREF_OS_PERMISSIONS_WARNING_DIVIDER);
        } else {
            Preference osWarning = findPreference(PREF_OS_PERMISSIONS_WARNING);
            Preference osWarningExtra = findPreference(PREF_OS_PERMISSIONS_WARNING_EXTRA);
            categoryWithWarning.configurePermissionIsOffPreferences(
                    osWarning,
                    osWarningExtra,
                    getContext(),
                    false,
                    getSiteSettingsDelegate().getAppName());
            if (osWarning.getTitle() == null) {
                preferenceScreen.removePreference(osWarning);
            } else if (osWarningExtra.getTitle() == null) {
                preferenceScreen.removePreference(osWarningExtra);
            }
        }
    }

    private void setupRelatedSitesPreferences() {
        PreferenceCategory relatedSitesHeader = findPreference(PREF_RELATED_SITES_HEADER);
        TextMessagePreference relatedSitesText = new TextMessagePreference(getContext(), null);
        boolean shouldRelatedSitesPrefBeVisible =
                getSiteSettingsDelegate().isPrivacySandboxFirstPartySetsUIFeatureEnabled()
                        && getSiteSettingsDelegate().isRelatedWebsiteSetsDataAccessEnabled()
                        && mSite.getRWSCookieInfo() != null;
        relatedSitesHeader.setVisible(shouldRelatedSitesPrefBeVisible);
        relatedSitesText.setVisible(shouldRelatedSitesPrefBeVisible);

        if (shouldRelatedSitesPrefBeVisible) {
            var rwsInfo = mSite.getRWSCookieInfo();
            relatedSitesText.setTitle(
                    getContext()
                            .getResources()
                            .getQuantityString(
                                    R.plurals.allsites_rws_summary,
                                    rwsInfo.getMembersCount(),
                                    Integer.toString(rwsInfo.getMembersCount()),
                                    rwsInfo.getOwner()));
            relatedSitesText.setManagedPreferenceDelegate(
                    new ForwardingManagedPreferenceDelegate(
                            getSiteSettingsDelegate().getManagedPreferenceDelegate()) {
                        @Override
                        public boolean isPreferenceControlledByPolicy(Preference preference) {
                            return getSiteSettingsDelegate()
                                    .isPartOfManagedRelatedWebsiteSet(
                                            mSite.getAddress().getOrigin());
                        }
                    });
            relatedSitesHeader.addPreference(relatedSitesText);

            if (getSiteSettingsDelegate().shouldShowPrivacySandboxRwsUi()) {
                relatedSitesHeader.removeAll();
                relatedSitesHeader.addPreference(relatedSitesText);
                for (Website site : mSite.getRWSCookieInfo().getMembers()) {
                    WebsiteRowPreference preference =
                            new RwsRowPreference(
                                    relatedSitesHeader.getContext(),
                                    getSiteSettingsDelegate(),
                                    site,
                                    getActivity().getLayoutInflater());
                    relatedSitesHeader.addPreference(preference);
                }
            }
        }
    }

    private void setUpAdsInformationalBanner() {
        // Add the informational banner which shows at the top of the UI if ad blocking is
        // activated on this site.
        boolean adBlockingActivated =
                SiteSettingsCategory.adsCategoryEnabled()
                        && WebsitePreferenceBridge.getAdBlockingActivated(
                                getSiteSettingsDelegate().getBrowserContextHandle(),
                                mSite.getAddress().getOrigin())
                        && findPreference(getPreferenceKey(ContentSettingsType.ADS)) != null;

        if (!adBlockingActivated) {
            removePreferenceSafely(PREF_INTRUSIVE_ADS_INFO);
            removePreferenceSafely(PREF_INTRUSIVE_ADS_INFO_DIVIDER);
        }
    }

    private SiteSettingsCategory getWarningCategory() {
        // If more than one per-app permission is disabled in Android, we can pick any category to
        // show the warning, because they will all show the same warning and all take the user to
        // the user to the same location. It is preferrable, however, that we give Geolocation some
        // priority because that category is the only one that potentially shows an additional
        // warning (when Location is turned off globally).
        BrowserContextHandle browserContextHandle =
                getSiteSettingsDelegate().getBrowserContextHandle();
        if (showWarningFor(SiteSettingsCategory.Type.DEVICE_LOCATION)) {
            return SiteSettingsCategory.createFromType(
                    browserContextHandle, SiteSettingsCategory.Type.DEVICE_LOCATION);
        } else if (showWarningFor(SiteSettingsCategory.Type.CAMERA)) {
            return SiteSettingsCategory.createFromType(
                    browserContextHandle, SiteSettingsCategory.Type.CAMERA);
        } else if (showWarningFor(SiteSettingsCategory.Type.MICROPHONE)) {
            return SiteSettingsCategory.createFromType(
                    browserContextHandle, SiteSettingsCategory.Type.MICROPHONE);
        } else if (showWarningFor(SiteSettingsCategory.Type.NOTIFICATIONS)) {
            return SiteSettingsCategory.createFromType(
                    browserContextHandle, SiteSettingsCategory.Type.NOTIFICATIONS);
        } else if (showWarningFor(SiteSettingsCategory.Type.NFC)) {
            return SiteSettingsCategory.createFromType(
                    browserContextHandle, SiteSettingsCategory.Type.NFC);
        } else if (showWarningFor(SiteSettingsCategory.Type.HAND_TRACKING)) {
            return SiteSettingsCategory.createFromType(
                    browserContextHandle, SiteSettingsCategory.Type.HAND_TRACKING);
        } else if (showWarningFor(SiteSettingsCategory.Type.AUGMENTED_REALITY)) {
            return SiteSettingsCategory.createFromType(
                    browserContextHandle, SiteSettingsCategory.Type.AUGMENTED_REALITY);
        }
        return null;
    }

    private boolean showWarningFor(@SiteSettingsCategory.Type int type) {
        BrowserContextHandle browserContextHandle =
                getSiteSettingsDelegate().getBrowserContextHandle();
        @ContentSettingValues
        Integer permission =
                mSite.getContentSetting(
                        browserContextHandle, SiteSettingsCategory.contentSettingsType(type));

        if (permission == null || permission == ContentSettingValues.BLOCK) {
            return false;
        }
        return SiteSettingsCategory.createFromType(browserContextHandle, type)
                .showPermissionBlockedMessage(getContext());
    }

    private boolean hasUsagePreferences() {
        // New actions under the Usage preference category must be listed here so that the category
        // heading can be removed when no actions are shown.
        return findPreference(PREF_CLEAR_DATA) != null;
    }

    private boolean hasPermissionsPreferences() {
        if (!mChooserPermissionPreferences.isEmpty() || !mEmbeddedPermissionPreferences.isEmpty()) {
            return true;
        }
        PreferenceScreen preferenceScreen = getPreferenceScreen();
        for (int i = 0; i < preferenceScreen.getPreferenceCount(); i++) {
            String key = preferenceScreen.getPreference(i).getKey();
            if (getContentSettingsTypeFromPreferenceKey(key) != ContentSettingsType.DEFAULT) {
                return true;
            }
        }
        return false;
    }

    private void setupContentSettingsPreference(
            Preference preference,
            @ContentSettingValues @Nullable Integer value,
            boolean isEmbargoed,
            boolean isOneTime) {
        if (value == null) return;
        setUpPreferenceCommon(preference, value);

        ChromeSwitchPreference switchPreference = (ChromeSwitchPreference) preference;
        switchPreference.setChecked(value == ContentSettingValues.ALLOW);
        switchPreference.setSummary(
                isEmbargoed
                        ? getString(R.string.automatically_blocked)
                        : getString(ContentSettingsResources.getCategorySummary(value, isOneTime)));
        switchPreference.setOnPreferenceChangeListener(this);
        @ContentSettingsType.EnumType
        int contentType = getContentSettingsTypeFromPreferenceKey(preference.getKey());
        if (contentType == mHighlightedPermission) {
            switchPreference.setBackgroundColor(
                    AppCompatResources.getColorStateList(getContext(), mHighlightColor)
                            .getDefaultColor());
        }
    }

    /**
     * Sets some properties that apply to both regular Preferences and ChromeSwitchPreferences, i.e.
     * preference title, enabled-state, and icon, based on the preference's key.
     */
    private void setUpPreferenceCommon(
            Preference preference, @ContentSettingValues @Nullable Integer value) {
        @ContentSettingsType.EnumType
        int contentType = getContentSettingsTypeFromPreferenceKey(preference.getKey());
        int titleResourceId = ContentSettingsResources.getTitle(contentType);

        if (titleResourceId != 0) {
            preference.setTitle(titleResourceId);
        }

        SiteSettingsCategory category =
                SiteSettingsCategory.createFromContentSettingsType(
                        getSiteSettingsDelegate().getBrowserContextHandle(), contentType);
        if (category != null
                && value != null
                && value != ContentSettingValues.BLOCK
                && !category.enabledInAndroid(getActivity())) {
            preference.setIcon(category.getDisabledInAndroidIcon(getContext()));
            preference.setEnabled(false);
        } else {
            preference.setIcon(getContentSettingsIcon(contentType, value));
        }

        // These preferences are persisted elsewhere, using SharedPreferences
        // can cause issues with keys matching up with value.
        preference.setPersistent(false);
        preference.setOrder(++mMaxPermissionOrder);
        getPreferenceScreen().addPreference(preference);
    }

    private void setUpLocationPreference(Preference preference) {
        @ContentSettingValues
        @Nullable
        Integer permission =
                mSite.getContentSetting(
                        getSiteSettingsDelegate().getBrowserContextHandle(),
                        ContentSettingsType.GEOLOCATION);
        if (setupAppDelegatePreference(
                preference,
                R.string.website_location_settings,
                ContentSettingsType.GEOLOCATION,
                permission)) {
            return;
        }

        setupContentSettingsPreference(
                preference,
                permission,
                mSite.isEmbargoed(ContentSettingsType.GEOLOCATION),
                isOneTime(ContentSettingsType.GEOLOCATION));
    }

    private void setUpSoundPreference(Preference preference) {
        if (!getArguments().getBoolean(EXTRA_SHOW_SOUND, true)) {
            return;
        }

        BrowserContextHandle browserContextHandle =
                getSiteSettingsDelegate().getBrowserContextHandle();
        @ContentSettingValues
        @Nullable
        Integer currentValue =
                mSite.getContentSetting(browserContextHandle, ContentSettingsType.SOUND);
        // In order to always show the sound permission, set it up with the default value if it
        // doesn't have a current value.
        if (currentValue == null) {
            currentValue =
                    WebsitePreferenceBridge.isCategoryEnabled(
                                    browserContextHandle, ContentSettingsType.SOUND)
                            ? ContentSettingValues.ALLOW
                            : ContentSettingValues.BLOCK;
        }
        // Not possible to embargo SOUND.
        setupContentSettingsPreference(
                preference,
                currentValue,
                /* isEmbargoed= */ false,
                isOneTime(ContentSettingsType.SOUND));
    }

    private void setUpJavascriptPreference(Preference preference) {
        BrowserContextHandle browserContextHandle =
                getSiteSettingsDelegate().getBrowserContextHandle();
        @ContentSettingValues
        @Nullable
        Integer currentValue =
                mSite.getContentSetting(browserContextHandle, ContentSettingsType.JAVASCRIPT);
        // If Javascript is blocked by default, then always show a Javascript permission.
        // To do this, set it to the default value (blocked).
        if ((currentValue == null)
                && !WebsitePreferenceBridge.isCategoryEnabled(
                        browserContextHandle, ContentSettingsType.JAVASCRIPT)) {
            currentValue = ContentSettingValues.BLOCK;
        }
        // Not possible to embargo JAVASCRIPT.
        setupContentSettingsPreference(
                preference,
                currentValue,
                /* isEmbargoed= */ false,
                isOneTime(ContentSettingsType.JAVASCRIPT));
    }

    /**
     * Updates the ads list preference based on whether the site is a candidate for blocking. This
     * has some custom behavior.
     * 1. If the site is a candidate and has activation, the permission should show up even if it
     *    is set as the default (e.g. |preference| is null).
     * 2. The BLOCK string is custom.
     */
    private void setUpAdsPreference(Preference preference) {
        BrowserContextHandle browserContextHandle =
                getSiteSettingsDelegate().getBrowserContextHandle();
        // Do not show the setting if the category is not enabled.
        if (!SiteSettingsCategory.adsCategoryEnabled()) {
            setupContentSettingsPreference(
                    preference, null, false, isOneTime(ContentSettingsType.ADS));
            return;
        }
        // If the ad blocker is activated, then this site will have ads blocked unless there is an
        // explicit permission disallowing the blocking.
        boolean activated =
                WebsitePreferenceBridge.getAdBlockingActivated(
                        browserContextHandle, mSite.getAddress().getOrigin());
        @ContentSettingValues
        @Nullable
        Integer permission = mSite.getContentSetting(browserContextHandle, ContentSettingsType.ADS);

        // If |permission| is null, there is no explicit (non-default) permission set for this site.
        // If the site is not considered a candidate for blocking, do the standard thing and remove
        // the preference.
        if (permission == null && !activated) {
            setupContentSettingsPreference(
                    preference, null, false, isOneTime(ContentSettingsType.ADS));
            return;
        }

        // However, if the blocking is activated, we still want to show the permission, even if it
        // is in the default state.
        if (permission == null) {
            permission =
                    WebsitePreferenceBridge.isCategoryEnabled(
                                    browserContextHandle, ContentSettingsType.ADS)
                            ? ContentSettingValues.ALLOW
                            : ContentSettingValues.BLOCK;
        }
        // Not possible to embargo ADS.
        setupContentSettingsPreference(
                preference,
                permission,
                /* isEmbargoed= */ false,
                isOneTime(ContentSettingsType.ADS));
    }

    private String getDSECategorySummary(@ContentSettingValues int value) {
        return value == ContentSettingValues.ALLOW
                ? getString(R.string.website_settings_permissions_allowed_dse)
                : getString(R.string.website_settings_permissions_blocked_dse);
    }

    public @ContentSettingsType.EnumType int getContentSettingsTypeFromPreferenceKey(
            String preferenceKey) {
        if (mPreferenceMap == null) {
            mPreferenceMap = new HashMap<>();
            for (@ContentSettingsType.EnumType int type = 0;
                    type <= ContentSettingsType.MAX_VALUE;
                    type++) {
                String key = getPreferenceKey(type);
                if (key != null) {
                    mPreferenceMap.put(key, type);
                }
            }
        }
        Integer type = mPreferenceMap.get(preferenceKey);
        if (type != null) return type;
        return ContentSettingsType.DEFAULT;
    }

    private void popBackIfNoSettings() {
        if (!hasPermissionsPreferences() && !hasUsagePreferences() && getActivity() != null) {
            getActivity().finish();
            if (mFromGrouped) {
                Activity groupActivity = GroupedWebsitesActivityHolder.getInstance().getActivity();
                if (groupActivity != null) groupActivity.finish();
            }
        }
    }

    @Override
    public boolean onPreferenceChange(Preference preference, Object newValue) {
        // It is possible that this UI is destroyed while a dialog is open because
        // incognito mode is closed through the system notification.
        if (getView() == null) return true;
        BrowserContextHandle browserContextHandle =
                getSiteSettingsDelegate().getBrowserContextHandle();
        int type = getContentSettingsTypeFromPreferenceKey(preference.getKey());
        if (type == ContentSettingsType.DEFAULT) return false;

        @ContentSettingValues int permission;
        if (newValue instanceof Boolean) {
            permission =
                    (Boolean) newValue ? ContentSettingValues.ALLOW : ContentSettingValues.BLOCK;
        } else {
            permission = (Integer) newValue;
        }

        mSite.setContentSetting(browserContextHandle, type, permission);
        // In Clank, one time grants are only possible via prompt, not via page
        // info.
        preference.setSummary(
                getString(ContentSettingsResources.getCategorySummary(permission, false)));
        preference.setIcon(getContentSettingsIcon(type, permission));

        if (mWebsiteSettingsObserver != null) {
            mWebsiteSettingsObserver.onPermissionChanged();
        }

        return true;
    }

    @Override
    public boolean onPreferenceClick(Preference preference) {
        if (mHideNonPermissionPreferences) {
            showResetPermissionsOnlyDialog();
        } else {
            showClearAndResetDialog();
        }
        return true;
    }

    /** Resets the current site, clearing all permissions and storage used (inc. cookies). */
    @VisibleForTesting
    public void resetSite() {
        if (getActivity() == null) return;
        // Clear the screen.
        // TODO(mvanouwerkerk): Refactor this class so that it does not depend on the screen state
        // for its logic. This class should maintain its own data model, and only update the screen
        // after a change is made.
        for (@ContentSettingsType.EnumType int type = 0;
                type <= ContentSettingsType.MAX_VALUE;
                type++) {
            String key = getPreferenceKey(type);
            if (key != null) {
                removePreferenceSafely(key);
            }
        }
        for (var preference : mEmbeddedPermissionPreferences) {
            getPreferenceScreen().removePreference(preference);
        }
        mEmbeddedPermissionPreferences.clear();

        // Clearing stored data implies popping back to parent menu if there is nothing left to
        // show. Therefore, we only need to explicitly close the activity if there's no stored data
        // to begin with. The only exception to this is if there are policy managed permissions as
        // those cannot be reset and will always show.
        boolean finishActivityImmediately =
                mSite.getTotalUsage() == 0 && !hasManagedChooserPermissions();

        SiteDataCleaner.resetPermissions(
                getSiteSettingsDelegate().getBrowserContextHandle(), mSite);
        SiteDataCleaner.clearData(getSiteSettingsDelegate(), mSite, mDataClearedCallback);

        RecordHistogram.recordEnumeratedHistogram(
                "Privacy.DeleteBrowsingData.Action",
                DeleteBrowsingDataAction.SITES_SETTINGS_PAGE,
                DeleteBrowsingDataAction.MAX_VALUE);
        if (finishActivityImmediately) {
            getActivity().finish();
            if (mFromGrouped) {
                Activity groupActivity = GroupedWebsitesActivityHolder.getInstance().getActivity();
                if (groupActivity != null) groupActivity.finish();
            }
        }
    }

    public boolean isOneTime(@ContentSettingsType.EnumType int type) {
        PermissionInfo permissionInfo = mSite.getPermissionInfo(type);
        return permissionInfo != null && permissionInfo.getSessionModel() == SessionModel.ONE_TIME;
    }

    /**
     * Ensures preference exists before removing to avoid NPE in
     * {@link PreferenceScreen#removePreference}.
     */
    private void removePreferenceSafely(CharSequence prefKey) {
        Preference preference = findPreference(prefKey);
        if (preference != null) getPreferenceScreen().removePreference(preference);
    }

    /** Removes any user granted chosen object preference(s) from the preference screen. */
    private void removeUserChosenObjectPreferences() {
        var it = mChooserPermissionPreferences.iterator();
        while (it.hasNext()) {
            var preference = it.next();
            if (preference != null && !((ChromeImageViewPreference) preference).isManaged()) {
                getPreferenceScreen().removePreference(preference);
                it.remove();
            }
        }

        if (hasManagedChooserPermissions()) {
            ManagedPreferencesUtils.showManagedSettingsCannotBeResetToast(getContext());
        }
    }

    private void showResetPermissionsOnlyDialog() {
        // Handle the reset preference click by showing a confirmation.
        mConfirmationDialog =
                new AlertDialog.Builder(getContext(), R.style.ThemeOverlay_BrowserUI_AlertDialog)
                        .setTitle(R.string.page_info_permissions_reset_dialog_title)
                        .setMessage(
                                getString(
                                        R.string.page_info_permissions_reset_confirmation,
                                        mSite.getAddress().getHost()))
                        .setPositiveButton(
                                R.string.reset,
                                (dialog, which) -> {
                                    SiteDataCleaner.resetPermissions(
                                            getSiteSettingsDelegate().getBrowserContextHandle(),
                                            mSite);
                                    if (mWebsiteSettingsObserver != null) {
                                        mWebsiteSettingsObserver.onPermissionsReset();
                                    }
                                })
                        .setNegativeButton(
                                R.string.cancel, (dialog, which) -> mConfirmationDialog = null)
                        .show();
    }

    private void showClearAndResetDialog() {
        // Handle a click on the Clear & Reset button.
        View dialogView =
                getActivity().getLayoutInflater().inflate(R.layout.clear_reset_dialog, null);
        TextView mainMessage = dialogView.findViewById(R.id.main_message);
        mainMessage.setText(
                getString(
                        R.string.website_single_reset_confirmation, mSite.getAddress().getHost()));
        TextView signedOutText = dialogView.findViewById(R.id.signed_out_text);
        signedOutText.setText(R.string.webstorage_clear_data_dialog_sign_out_message);
        TextView offlineText = dialogView.findViewById(R.id.offline_text);
        offlineText.setText(R.string.webstorage_delete_data_dialog_offline_message);
        mConfirmationDialog =
                new AlertDialog.Builder(getContext(), R.style.ThemeOverlay_BrowserUI_AlertDialog)
                        .setView(dialogView)
                        .setTitle(R.string.website_reset_confirmation_title)
                        .setPositiveButton(
                                R.string.website_reset,
                                (dialog, which) -> {
                                    resetSite();
                                    if (mWebsiteSettingsObserver != null) {
                                        mWebsiteSettingsObserver.onPermissionsReset();
                                    }
                                })
                        .setNegativeButton(
                                R.string.cancel, (dialog, which) -> mConfirmationDialog = null)
                        .show();
    }

    boolean hasManagedChooserPermissions() {
        for (var preference : mChooserPermissionPreferences) {
            if (preference.isManaged()) {
                return true;
            }
        }
        return false;
    }
}