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

// Copyright 2022 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 android.app.Activity;
import android.app.Dialog;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;

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.CustomDividerFragment;
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;

/** Shows the permissions and other settings for a group of websites. */
public class GroupedWebsitesSettings extends BaseSiteSettingsFragment
        implements SettingsPage, Preference.OnPreferenceClickListener, CustomDividerFragment {
    public static final String EXTRA_GROUP = "org.chromium.chrome.preferences.site_group";

    // Preference keys, see grouped_websites_preferences.xml.
    public static final String PREF_SITE_TITLE = "site_title";
    public static final String PREF_CLEAR_DATA = "clear_data";
    public static final String PREF_RELATED_SITES = "related_sites";
    public static final String PREF_SITES_IN_GROUP = "sites_in_group";
    public static final String PREF_RESET_GROUP = "reset_group_button";

    private WebsiteGroup mSiteGroup;

    private Dialog mConfirmationDialog;

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

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

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        init();
        super.onActivityCreated(savedInstanceState);
    }

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

    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;
        }

        WebsiteGroup extraGroup = (WebsiteGroup) getArguments().getSerializable(EXTRA_GROUP);
        assert extraGroup != null : "EXTRA_GROUP must be provided.";
        mSiteGroup = extraGroup;
        var domainAndRegistry = extraGroup.getDomainAndRegistry();

        // Set title
        Activity activity = getActivity();
        mPageTitle.set(activity.getString(R.string.domain_settings_title, domainAndRegistry));

        // Preferences screen
        SettingsUtils.addPreferencesFromResource(this, R.xml.grouped_websites_preferences);
        findPreference(PREF_SITE_TITLE).setTitle(domainAndRegistry);
        findPreference(PREF_SITES_IN_GROUP)
                .setTitle(
                        activity.getString(
                                R.string.domain_settings_sites_in_group, domainAndRegistry));
        setUpClearDataPreference();
        setUpResetGroupPreference();
        setUpRelatedSitesPreferences();
        updateSitesInGroup();
    }

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

    @Override
    public boolean onPreferenceTreeClick(Preference preference) {
        if (preference instanceof WebsiteRowPreference) {
            // Handle a click on one of the sites in this group.
            // Save the current activity, so it's accessible from the SingleWebsiteSettings.
            GroupedWebsitesActivityHolder.getInstance().setActivity(getActivity());
            ((WebsiteRowPreference) preference)
                    .handleClick(getArguments(), /* fromGrouped= */ true);
        }
        return super.onPreferenceTreeClick(preference);
    }

    @Override
    public boolean onPreferenceClick(Preference preference) {
        // 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_group_reset_confirmation,
                        mSiteGroup.getDomainAndRegistry()));
        TextView signedOutText = dialogView.findViewById(R.id.signed_out_text);
        signedOutText.setText(R.string.webstorage_clear_data_dialog_sign_out_group_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) -> {
                                    resetGroup();
                                })
                        .setNegativeButton(
                                R.string.cancel, (dialog, which) -> mConfirmationDialog = null)
                        .show();
        return true;
    }

    @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(), mSiteGroup, mDataClearedCallback);
                        }
                    };
            ClearWebsiteStorageDialog dialogFragment =
                    ClearWebsiteStorageDialog.newInstance(
                            preference, onDialogClosed, /* isGroup= */ true);
            dialogFragment.setTargetFragment(this, 0);
            dialogFragment.show(getFragmentManager(), ClearWebsiteStorageDialog.TAG);
        } else {
            super.onDisplayPreferenceDialog(preference);
        }
    }

    private final Runnable mDataClearedCallback =
            () -> {
                Activity activity = getActivity();
                if (activity == null || activity.isFinishing()) {
                    return;
                }
                // TODO(crbug.com/40231223): This always navigates the user back to the "All sites"
                // page regardless of whether there are any non-resettable permissions left in the
                // sites within the group. Consider calculating those and refreshing the screen in
                // place for a slightly smoother user experience. However, due to the complexity
                // involved in refreshing the already fetched data and a very marginal benefit, it
                // may not be worth it.
                getActivity().finish();
            };

    @VisibleForTesting
    public void resetGroup() {
        if (getActivity() == null) return;
        SiteDataCleaner.resetPermissions(
                getSiteSettingsDelegate().getBrowserContextHandle(), mSiteGroup);

        RecordHistogram.recordEnumeratedHistogram(
                "Privacy.DeleteBrowsingData.Action",
                DeleteBrowsingDataAction.SITES_SETTINGS_PAGE,
                DeleteBrowsingDataAction.MAX_VALUE);

        SiteDataCleaner.clearData(getSiteSettingsDelegate(), mSiteGroup, mDataClearedCallback);
    }

    private void setUpClearDataPreference() {
        ClearWebsiteStorage preference = findPreference(PREF_CLEAR_DATA);
        long storage = mSiteGroup.getTotalUsage();
        int cookies = mSiteGroup.getNumberOfCookies();
        if (storage > 0 || cookies > 0) {
            preference.setTitle(
                    SiteSettingsUtil.generateStorageUsageText(
                            preference.getContext(), storage, cookies));
            preference.setDataForDisplay(
                    mSiteGroup.getDomainAndRegistry(),
                    mSiteGroup.hasInstalledApp(
                            getSiteSettingsDelegate().getOriginsWithInstalledApp()),
                    /* isGroup= */ true);
            if (mSiteGroup.isCookieDeletionDisabled(
                    getSiteSettingsDelegate().getBrowserContextHandle())) {
                preference.setEnabled(false);
            }
        } else {
            getPreferenceScreen().removePreference(preference);
        }
    }

    private void setUpResetGroupPreference() {
        Preference preference = findPreference(PREF_RESET_GROUP);
        if (mSiteGroup.isCookieDeletionDisabled(
                getSiteSettingsDelegate().getBrowserContextHandle())) {
            preference.setEnabled(false);
        }
        preference.setOnPreferenceClickListener(this);
    }

    private void setUpRelatedSitesPreferences() {
        PreferenceCategory relatedSitesHeader = findPreference(PREF_RELATED_SITES);
        TextMessagePreference relatedSitesText = new TextMessagePreference(getContext(), null);
        boolean shouldRelatedSitesPrefBeVisible =
                getSiteSettingsDelegate().isPrivacySandboxFirstPartySetsUIFeatureEnabled()
                        && getSiteSettingsDelegate().isRelatedWebsiteSetsDataAccessEnabled()
                        && mSiteGroup.getRWSInfo() != null;
        relatedSitesText.setVisible(shouldRelatedSitesPrefBeVisible);
        relatedSitesHeader.setVisible(shouldRelatedSitesPrefBeVisible);

        if (shouldRelatedSitesPrefBeVisible) {
            var rwsInfo = mSiteGroup.getRWSInfo();

            relatedSitesText.setTitle(
                    getContext()
                            .getResources()
                            .getQuantityString(
                                    R.plurals.allsites_rws_summary,
                                    mSiteGroup.getRWSInfo().getMembersCount(),
                                    Integer.toString(mSiteGroup.getRWSInfo().getMembersCount()),
                                    rwsInfo.getOwner()));
            relatedSitesText.setManagedPreferenceDelegate(
                    new ForwardingManagedPreferenceDelegate(
                            getSiteSettingsDelegate().getManagedPreferenceDelegate()) {
                        @Override
                        public boolean isPreferenceControlledByPolicy(Preference preference) {
                            for (var site : mSiteGroup.getWebsites()) {
                                if (getSiteSettingsDelegate()
                                        .isPartOfManagedRelatedWebsiteSet(
                                                site.getAddress().getOrigin())) {
                                    return true;
                                }
                            }
                            return false;
                        }
                    });
            relatedSitesHeader.addPreference(relatedSitesText);

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

    private void updateSitesInGroup() {
        PreferenceCategory category = findPreference(PREF_SITES_IN_GROUP);
        category.removeAll();
        for (Website site : mSiteGroup.getWebsites()) {
            WebsiteRowPreference preference =
                    new WebsiteRowPreference(
                            category.getContext(),
                            getSiteSettingsDelegate(),
                            site,
                            getActivity().getLayoutInflater());
            preference.setOnDeleteCallback(
                    () -> {
                        category.removePreference(preference);
                    });
            category.addPreference(preference);
        }
    }
}